薛亮的主页

  • 首页

  • 标签

  • 归档

  • 搜索

MySQL 多列索引优化小记

发表于 2017-03-26 | 更新于 2018-12-01 | | 阅读次数: | 阅读人数:

MySQL 5.6.30

问题背景

由于爬虫抓取的数据不断增多,这两天在不断对数据库以及查询语句进行优化,其中一个表结构如下:

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `newspaper_article` (
`id` varchar(50) NOT NULL COMMENT '编号',
`title` varchar(190) NOT NULL COMMENT '标题',
`author` varchar(255) DEFAULT NULL COMMENT '作者',
`date` date NULL DEFAULT NULL COMMENT '发表时间',
`content` longtext COMMENT '正文',
`status` tinyint(4) DEFAULT '0',
PRIMARY KEY (`id`),
KEY `idx_status_date` (`status`,`date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章表';

根据业务需要,添加了 idx_status_date 索引,在执行下面这个 SQL 时特别耗时:

1
SELECT id, title, status, date FROM newspaper_article WHERE status > -2 AND date = '2016-01-07';

根据观察,每天新增的数据大概在2500条以内,本以为这里指定了具体某天的日期 '2016-01-07' ,实际需要扫描的数据量应该在2500条以内才对,但实际并非如此:
EXPLAIN
实际共扫描了185589条数据,远远高于预估的2500条,且实际执行时间都将近3秒钟:

EXPLAIN

这是为什么呢?

解决方案

将 idx_status_date (status, date) 改为 idx_status (status) 后,查看 MySQL 执行计划:

EXPLAIN

可以看到将多列索引改为单列索引后,执行计划要扫描的数据总量没有任何变化。结合多列索引遵循最左前缀原则,推测上面的查询语句只使用了 idx_status_date 最左边的 status 的索引。

翻了下《高性能MySQL》找到了下面这段话,证实了我的想法:

如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查找。例如有查询 WHERE last_name = 'Smith' AND first_name LIKE 'J%' AND dob = '1976-12-23' ,这个查询只能使用索引的前两列,因为这里 LIKE 是一个范围条件(但是服务器可以把其余列用于其他目的)。如果范围查询列值的数量有限,那么可以通过使用多个等于条件来代替范围条件。

因此,这里解决思路有两种:

  • 可以通过使用多个等于条件来代替范围条件
  • 修改 idx_status_date (status, date) 为索引 idx_date_status (date, status) ,并新建一个 idx_status 索引,即可达到同样的效果。

优化后的执行计划:

EXPLAIN

实际执行结果:

EXPLAIN

总结

当人们谈论索引的时候,如果没有特别指明类型,那么多半说的是 B-Tree 索引,它使用 B-Tree 数据结构来存储数据。我们使用术语“B-Tree”,是因为 MySQL 在 CREATE TABLE 和其他语句中也使用该关键字。不过,底层的存储引擎也可能使用不同的存储结构。InnoDB使用的是B+Tree。
假如有如下数据表:

1
2
3
4
5
6
7
CREATE TABLE People (
last_name varchar(50) not null,
first_name varchar(50) not null,
dob date not null,
gender enum('m', 'f') not null,
key(last_name, first_name, dob)
);

B-Tree 索引对如下类型的查询有效

  • 全值匹配
    全值匹配指的是和索引中的所有列进行匹配,例如上表的索引可用于查找姓名为 Cuba Allen 、出生于 1960-01-01 的人。
  • 匹配最左前缀
    上表中的索引可用于查找所有姓为 Allen 的人,即只使用索引的第一列。
  • 匹配列前缀
    只匹配某一列的值的开头部分。例如上表的索引可用于查找所有以 J 开头的姓的人。这里也只使用了索引的第一列。
  • 匹配范围值
    例如上表中的索引可用于查找姓在 Allen 和 Barrymore 之间的人。这里也只使用了索引的第一列。
  • 精确匹配某一列并范围匹配另外一列
    上表的索引也可用于查找所有姓为 Allen ,并且名字是字母 K 开头(比如 Kim 、 Karl 等)的人。即第一列 last_name 全匹配,第二列 first_name 范围匹配。
  • 只访问索引的查询
    B-Tree 通常可以支持“只访问索引的查询”,即查询只需要访问索引,而无须访问数据行。

B-Tree 索引的一些限制

  • 如果不是按照索引的最左列开始查找,则无法使用索引。例如上表的索引无法用于查找名字为 Bill 的人,也无法查找某个特定生日的人,因为这两列都不是最左数据列。类似地,也无法查找姓氏以某个字母结尾的人。
  • 不能跳过索引中列。也就是说,上表的索引无法用于查找姓氏为 Smith 并且在某个特定日期出生的人。如果不指定名(first_name),则 MySQL 只能使用索引的第一列。
  • 如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查找。例如有查询 WHERE last_name = 'Smith' AND first_name LIKE 'J%' AND dob = '1976-12-23' ,这个查询只能使用索引的前两列,因为这里 LIKE 是一个范围条件(但是服务器可以把其余列用于其他目的)。如果范围查询列值的数量有限,那么可以通过使用多个等于条件来代替范围条件。

相关文章

  • MySQL 中 AUTO_INCREMENT 的“坑”
  • MySQL主从架构配置详解
  • MySQL中字符串、日期时间等常用函数总结
  • MySQL 工作笔记(持续更新)

Spring MVC + Security 4 初体验(Java配置版)

发表于 2017-03-06 | 更新于 2018-12-01 | | 阅读次数: | 阅读人数:

spring Version = 4.3.6.RELEASE
springSecurityVersion = 4.2.1.RELEASE
Gradle 3.0 + Eclipse Neno(4.6)

这篇文章同样是使用的Java配置,而非XML配置,如果你对于Java配置的Spring MVC开发还不太熟悉,可以先看我这篇文章。

Authority

创建一个 Authority ,实现自 org.springframework.security.core.GrantedAuthority 类,getAuthority 方法只返回一个表示权限名称的字符串,如 AUTH_USER 、 AUTH_ADMIN 、 AUTH_DBA 等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Authority implements GrantedAuthority {

private static final long serialVersionUID = 1L;

private String authority;

public Authority() { }
public Authority(String authority) {
this.setAuthority(authority);
}

@Override
public String getAuthority() {
return this.authority;
}
public void setAuthority(String authority) {
this.authority = authority;
}
}

User

User 类实现自 org.springframework.security.core.userdetails.UserDetails 接口,包含一组权限的集合 authorities。

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
public class User implements UserDetails {

private static final long serialVersionUID = 1L;

private String username;
private String password;
private List<Authority> authorities;

@Override
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}

@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
public void setAuthorities(List<Authority> authorities) {
this.authorities = authorities;
}

@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}

UserDetailsService

MyUserDetailsService 实现了 org.springframework.security.core.userdetails.UserDetailsService 的 loadUserByUsername 方法,该方法根据用户名查询符合条件的用户,若没有找到符合条件的用户,必须抛出 UsernameNotFoundException 异常,而不能返回空。这里可以调用 DAO 层,从数据库查询用户,我为了简单,直接将用户临时放到一个常量内,模拟从数据库查询用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class MyUserDetailsService implements UserDetailsService {

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<User> userList = Constants.userList;
for (int i = 0, len = userList.size(); i < len; i++) {
User user = userList.get(i);
if (user.getUsername().equals(username)) {
return user;
}
}
throw new UsernameNotFoundException("用户不存在!");
}

}

SecurityConfig

SecurityConfig 类继承 org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter,WebSecurityConfigurerAdapter 提供了一些默认的配置,方便创建一个实例。

进入 configure 方法中,首先允许任何情况下的对csrfTokenApi 的请求,该 API 返回一个 csrfToken ,默认情况下除 GET、HEAD、TRACE 和 OPTIONS外,所有请求都必须经过 CSRF 认证。接下来对不同的API请求设置不同的权限,并且确保所有对 /api/ 下的请求都经过了认证。
这里向 access 方法传递的表达式中的权限名称,对应上面提到的 Authority 类中 getAuthority 返回的字符串的值,详细的表达式介绍,请移步至这里。

接着,对登录表单进行配置。通过 loginProcessingUrl 配置表单提交地址,这个地址对应的API不需要自己写,Spring Security 会自动拦截提交到此地址请求,将其视为登录请求。如果希望登录成功后通过服务器转发到其他页面,可以调用 successForwardUrl(String forwardUrl) 方法指定跳转的地址,对应地,指定失败后跳转地址的方法是 failureForwardUrl(String forwardUrl)。

这里我使用了RESTful,故不需要配置服务端的转发,而是配置了另外两处:successHandler 和 failureHandler ,successHandler 方法接收一个 AuthenticationSuccessHandler 对象,认证通过之后,Spring Security 将调用该对象的 onAuthenticationSuccess 方法,类似地,failureHandler 方法接收一个 AuthenticationFailureHandler 对象,认证失败之后,将调用该对象的 onAuthenticationFailure 方法。

配置完登录相关信息之后,接着配置和登出有关的信息。和配置登录表单提交地址类似,这里需要配置登出请求提交地址,这里调用 logoutUrl 方法,指定登出的链接地址,该地址和前面提到的 loginProcessingUrl 都不需要自己写,这两个都是全权交由 Spring Security 来处理。当用户请求 logoutUrl 方法指定的地址时,Spring Security 将对用户执行登出操作。和前面提到的 successForwardUrl 类似,这里提供了 logoutSuccessUrl 方法指定登出成功之后转发的地址。不过我用了RESTful,就不再调用此方法,而是调用 logoutSuccessHandler 传入 LogoutSuccessHandler 对象,登出成功后将调用该对象的 onLogoutSuccess 方法。

最后,配置对异常的处理 exceptionHandling ,和上面介绍的 successHandler 、 failureHandler 以及 logoutSuccessHandler 差不多,authenticationEntryPoint 接收一个 AuthenticationEntryPoint 对象,当用户请求的操作需要登录时,将抛出 AuthenticationException 异常,并且将该异常传入到 AuthenticationEntryPoint 对象的 commence 方法。
accessDeniedHandler 方法接收一个 AccessDeniedHandler 对象,该对象的 handle 方法将在权限不足时调用。

配置完这些,看 configureGlobalSecurity 方法,给 AuthenticationManagerBuilder 配置一个 UserDetailsService 对象,当用户执行登录时,Spring Security 将调用该对象的 loadUserByUsername 方法,将 username 传入此方法,根据 username 获取一个 UserDetails 对象。

另外,由于不能在数据库中保存明文密码,这里对密码进行 bcrypt 加密后保存,验证密码是否正确时,需要对用户输入的明文密码进行 bcrypt 加密后比较密文是否一致,故这里需要提供一个 BCryptPasswordEncoder 对象。

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
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Value("${api.csrftoken}")
private String csrfTokenApi;

@Value("${api.login}")
private String loginApi;

@Value("${api.logout}")
private String logoutApi;

@Autowired
private MyUserDetailsService userDetailsService;

@Autowired
private PasswordEncoder passwordEncoder;

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers(csrfTokenApi).permitAll()
.antMatchers("/api/user/**").access("hasAuthority('USER')")
.antMatchers("/api/admin/**").access("hasAuthority('ADMIN')")
.antMatchers("/api/dba/**").access("hasAuthority('DBA')")
.antMatchers("/api/**").fullyAuthenticated()
.and().formLogin().loginProcessingUrl(loginApi)
.successHandler(new RestAuthenticationSuccessHandler())
.failureHandler(new RestAuthenticationFailureHandler())
.and().logout().logoutUrl(logoutApi)
.logoutSuccessHandler(new RestLogoutSuccessHandler())
.and().exceptionHandling().authenticationEntryPoint(new RestAuthenticationEntryPoint())
.accessDeniedHandler(new RestAccessDeniedHandler());
}

@Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}

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

WebAppConfig

因为采用RESTful风格,这里配置响应视图为json格式。

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
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "org.xueliang.springsecuritystudy")
@PropertySource({"classpath:config.properties"})
public class WebAppConfig extends WebMvcConfigurerAdapter {

@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(@Autowired MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter, @Autowired ContentNegotiationManager mvcContentNegotiationManager) {
RequestMappingHandlerAdapter requestMappingHandlerAdapter = new RequestMappingHandlerAdapter();
requestMappingHandlerAdapter.setMessageConverters(Collections.singletonList(mappingJackson2HttpMessageConverter));
requestMappingHandlerAdapter.setContentNegotiationManager(mvcContentNegotiationManager);
return requestMappingHandlerAdapter;
}

@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
return new MappingJackson2HttpMessageConverter();
}

/**
* 设置欢迎页
* 相当于web.xml中的 welcome-file-list > welcome-file
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addRedirectViewController("/", "/index.html");
}
}

WebAppInitializer

Spring Security 架构是完全基于标准的 Servlet 过滤器的,这里我们需要在 WebInitializer 中引入 DelegatingFilterProxy 过滤器。

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
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
servletContext.addFilter("springSecurityFilterChain", new DelegatingFilterProxy("springSecurityFilterChain")).addMappingForUrlPatterns(null, false, "/api/*");
// 静态资源映射
servletContext.getServletRegistration("default").addMapping("*.html", "*.ico");
super.onStartup(servletContext);
}

@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] { WebAppConfig.class };
}

@Override
protected Class<?>[] getServletConfigClasses() {
return null;
}

@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}

@Override
protected Filter[] getServletFilters() {
return new Filter[] { new CharacterEncodingFilter("UTF-8", true) };
}
}

Source

本文使用到的项目源码已经放到 Github 上,你可以下载后运行。

相关文章

  • Spring RESTful + Redis全注解实现恶意登录保护机制
  • Java程序通过代理访问网络

Java程序通过代理访问网络

发表于 2017-01-16 | 更新于 2018-12-01 | | 阅读次数: | 阅读人数:

问题背景

最近工作上有开发爬虫的任务,对目标网站数据进行抓取,由于大部分网站都在国外,无法直接访问,需要通过代理才能登录。爬虫部署的服务器在香港,所以爬虫部署到服务器后,是可以访问目标网站的,但本地开发调试程序时,需要通过代理才能访问。
这篇文章就带大家了解一下如何在Java程序中使用代理访问网络。

解决方案

  • 你需要一个代理服务器,和一个可以连接到此服务器的客户端。
    花点银子买一个稳定的账号,或者自己搭建一个。
    这里我使用自己搭建的 Shadowsocks 代理服务器,使用 Shadowsocks-Windows 作为本地代理的客户端,并开启默认的 1080 端口,以供本地其他程序通过代理访问网络。
    编辑服务器信息

  • 指定 Java 程序的代理服务器地址和端口
    有两种指定方式:

    1. 通过 命令行参数 指定
      如果只需要考虑代理 HTTP 协议请求,只需添加如下命令行参数:

      1
      -Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=1080

      想要 HTTP 和 HTTPS 协议的请求都通过代理访问网络,可以追加上:

      1
      -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=1080

      最终填写的值为:

      1
      -Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=1080 -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=1080
    2. 在程序中使用System.setProperty(String, String)
      同样很简单,这里直接上代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      String proxyHost = "127.0.0.1";
      String proxyPort = "1080";

      System.setProperty("http.proxyHost", proxyHost);
      System.setProperty("http.proxyPort", proxyPort);

      // 对https也开启代理
      System.setProperty("https.proxyHost", proxyHost);
      System.setProperty("https.proxyPort", proxyPort);

推荐使用第一种方案,通过VM Option 的方式,对代码没有任何侵入,绿色环保。

测试

这里我在Eclipse中使用第一种方法进行测试。

  • 测试代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.URL;
    import java.net.URLConnection;

    public class Test {

    public static void main(String[] args) throws IOException {
    URL url = new URL("https://google.com");
    URLConnection connection = url.openConnection();
    connection.connect();
    InputStream inputStream = connection.getInputStream();
    byte[] bytes = new byte[1024];
    while (inputStream.read(bytes) >= 0) {
    System.out.println(new String(bytes));
    }
    }
    }
  • 测试结果,可以正常访问Google等网站。
    Java通过代理访问google.com结果

总结

除了上述 http.proxyHost 和 http.proxyPort,以及 https.proxyHost 和 https.proxyPort 在代理时比较有用外,还有一个属性也比较有用,那就是 http.nonProxyHosts,它用来指定哪些主机不使用代理,如果有多个,用英文竖线(|)分隔,可以使用星号 (*)作为通配符。
下表是常用协议对应的代理属性:

























































协议 属性(代理主机/代理端口/不使用代理的主机列表) 默认值
HTTP http.proxyHost <none>
http.proxyPort 80
http.nonProxyHosts <none>
HTTPS https.proxyHost <none>
https.proxyPort 443
https.nonProxyHosts <none>
FTP ftp.proxyHost <none>
ftp.proxyPort 80
ftp.nonProxyHosts <none>
SOCKS socksProxyHost <none>
socksProxyPort 1080

详细介绍请参考官方说明:Java Networking and Proxies

Spring RESTful + Redis全注解实现恶意登录保护机制

发表于 2016-11-02 | 更新于 2018-12-01 | | 阅读次数: | 阅读人数:

好久没更博了…
最近看了个真正全注解实现的 SpringMVC 博客,感觉很不错,终于可以彻底丢弃 web.xml 了。其实这玩意也是老东西了,丢弃 web.xml,是基于 5、6年前发布的 Servlet 3.0 规范,只不过少有人玩而已…现在4.0都快正式发布了…Spring对注解的支持也从09年底就开始支持了…
基础部分我就不仔细讲了,可以先看一下这篇 以及其中提到的另外两篇文章,这三篇文章讲的很不错。
下面开始旧东西新玩~~~

构建

项目是基于 gradle 3.1构建的,这是项目依赖:

1
2
3
4
5
6
7
8
9
dependencies {
def springVersion = '4.3.2.RELEASE'

compile "org.springframework:spring-web:$springVersion"
compile "org.springframework:spring-webmvc:$springVersion"
compile "redis.clients:jedis:2.9.0"
compile "javax.servlet:javax.servlet-api:3.1.0"
compile "org.json:json:20160810"
}

编写Java版的web.xml

想要让请求经过Java,少不了配置 web.xml,不过现在我们来写个Java版的~
这里和传统的 web.xml 一样,依次添加 filter, servlet 。

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
package org.xueliang.loginsecuritybyredis.commons;

import javax.servlet.FilterRegistration;
import javax.servlet.Servlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.servlet.DispatcherServlet;

/**
* 基于注解的/WEB-INF/web.xml
* 依赖 servlet 3.0
* @author XueLiang
* @date 2016年10月24日 下午5:58:45
* @version 1.0
*/
public class CommonInitializer implements WebApplicationInitializer {

@Override
public void onStartup(ServletContext servletContext) throws ServletException {

// 基于注解配置的Web容器上下文
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(WebAppConfig.class);

// 添加编码过滤器并进行映射
CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter("UTF-8", true);
FilterRegistration.Dynamic dynamicFilter = servletContext.addFilter("characterEncodingFilter", characterEncodingFilter);
dynamicFilter.addMappingForUrlPatterns(null, true, "/*");

// 添加静态资源映射
ServletRegistration defaultServletRegistration = servletContext.getServletRegistration("default");
defaultServletRegistration.addMapping("*.html");

Servlet dispatcherServlet = new DispatcherServlet(context);
ServletRegistration.Dynamic dynamicServlet = servletContext.addServlet("dispatcher", dispatcherServlet);
dynamicServlet.addMapping("/");
}
}

这一步走完,Spring 基本上启动起来了。

编写Java版的Spring配置

现在Spring已经可以正常启动了,但我们还要给 Spring 做一些配置,以便让它按我们需要的方式工作~
这里因为后端只负责提供数据,而不负责页面渲染,所以只需要配置返回 json 视图即可,个人比较偏爱采用内容协商,所以这里我使用了 ContentNegotiationManagerFactoryBean,但只配置了一个 JSON 格式的视图。
为了避免中文乱码,这里设置了 StringHttpMessageConverter 默认编码格式为 UTF-8,然后将其设置为 RequestMappingHandlerAdapter 的消息转换器。
最后还需要再配置一个欢迎页,类似于 web.xml 的 welcome-file-list - welcome-file,因为 Servlet 3.0 规范没有针对欢迎页的Java配置方案,所以目前只能在Java中这样配置,其效果类似于在XML版中配置 <mvc:redirect-view-controller path="/" redirect-url="/index.html"/> 。
最后注意这里的 @Bean 注解,默认的 name 是方法名。

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
package org.xueliang.loginsecuritybyredis.commons;

import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Properties;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.http.MediaType;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.ContentNegotiationManagerFactoryBean;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "org.xueliang.loginsecuritybyredis")
@PropertySource({"classpath:loginsecuritybyredis.properties"})
public class WebAppConfig extends WebMvcConfigurerAdapter {

/**
* 内容协商
* @return
*/
@Bean
public ContentNegotiationManager mvcContentNegotiationManager() {
ContentNegotiationManagerFactoryBean contentNegotiationManagerFactoryBean = new ContentNegotiationManagerFactoryBean();
contentNegotiationManagerFactoryBean.setFavorParameter(true);
contentNegotiationManagerFactoryBean.setIgnoreAcceptHeader(true);
contentNegotiationManagerFactoryBean.setDefaultContentType(MediaType.APPLICATION_JSON_UTF8);
Properties mediaTypesProperties = new Properties();
mediaTypesProperties.setProperty("json", MediaType.APPLICATION_JSON_UTF8_VALUE);
contentNegotiationManagerFactoryBean.setMediaTypes(mediaTypesProperties);
contentNegotiationManagerFactoryBean.afterPropertiesSet();
return contentNegotiationManagerFactoryBean.getObject();
}

@Bean
public ContentNegotiatingViewResolver contentNegotiatingViewResolver(@Autowired ContentNegotiationManager mvcContentNegotiationManager) {
ContentNegotiatingViewResolver contentNegotiatingViewResolver = new ContentNegotiatingViewResolver();
contentNegotiatingViewResolver.setOrder(1);
contentNegotiatingViewResolver.setContentNegotiationManager(mvcContentNegotiationManager);
return contentNegotiatingViewResolver;
}

/**
* 采用UTF-8编码,防止中文乱码
* @return
*/
@Bean
public StringHttpMessageConverter stringHttpMessageConverter() {
return new StringHttpMessageConverter(Charset.forName("UTF-8"));
}

@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(@Autowired StringHttpMessageConverter stringHttpMessageConverter) {
RequestMappingHandlerAdapter requestMappingHandlerAdapter = new RequestMappingHandlerAdapter();
requestMappingHandlerAdapter.setMessageConverters(Collections.singletonList(stringHttpMessageConverter));
return requestMappingHandlerAdapter;
}

/**
* 设置欢迎页
* 相当于web.xml中的 welcome-file-list > welcome-file
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addRedirectViewController("/", "/index.html");
}
}

编写登录认证Api

这里在 init 方法中初始化几个用户,放入 USER_DATA 集合,用于后续模拟登录。然后初始化 jedis 连接信息。init 方法被 @PostConstruct 注解,因此 Spring 创建该类的对象后,将自动执行其 init 方法,进行初始化操作。
然后看 login 方法,首先根据用户名获取最近 MAX_DISABLED_SECONDS 秒内失败的次数,是否超过最大限制 MAX_TRY_COUNT。

若超过最大限制,不再对用户名和密码进行认证,直接返回认证失败提示信息,也即账户已被锁定的提示信息。

否则,进行用户认证。

若认证失败,将其添加到 Redis 缓存中,并设置过期默认为 MAX_DISABLED_SECONDS,表示从此刻起,MAX_DISABLED_SECONDS 秒内,该用户已登录失败 count 次。

若Redis缓存中已存在该用户认证失败的计数信息,则刷新 count 值,并将旧值的剩余存活时间设置到新值上,然后返回认证失败提示信息。

否则,返回认证成功提示信息。

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
package org.xueliang.loginsecuritybyredis.web.controller.api;

import java.util.HashMap;
import java.util.Map;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.xueliang.loginsecuritybyredis.web.model.JSONResponse;
import org.xueliang.loginsecuritybyredis.web.model.User;

import redis.clients.jedis.Jedis;

/**
* 认证类
* @author XueLiang
* @date 2016年11月1日 下午4:11:59
* @version 1.0
*/
@RestController
@RequestMapping("/api/auth/")
public class AuthApi {

private static final Map<String, User> USER_DATA = new HashMap<String, User>();
@Value("${auth.max_try_count}")
private int MAX_TRY_COUNT = 0;
@Value("${auth.max_disabled_seconds}")
private int MAX_DISABLED_SECONDS = 0;

@Value("${redis.host}")
private String host;
@Value("${redis.port}")
private int port;
private Jedis jedis;

@PostConstruct
public void init() {
for (int i = 0; i < 3; i++) {
String username = "username" + 0;
String password = "password" + 0;
USER_DATA.put(username + "_" + password, new User(username, "nickname" + i));
}
jedis = new Jedis(host, port);
}

@RequestMapping(value = {"login"}, method = RequestMethod.POST)
public String login(@RequestParam("username") String username, @RequestParam("password") String password) {
JSONResponse jsonResponse = new JSONResponse();
String key = username;
String countString = jedis.get(key);
boolean exists = countString != null;
int count = exists ? Integer.parseInt(countString) : 0;
if (count >= MAX_TRY_COUNT) {
checkoutMessage(key, count, jsonResponse);
return jsonResponse.toString();
}
User user = USER_DATA.get(username + "_" + password);
if (user == null) {
count++;
int secondsRemain = MAX_DISABLED_SECONDS;
if (exists && count < 5) {
secondsRemain = (int)(jedis.pttl(key) / 1000);
}
jedis.set(key, count + "");
jedis.expire(key, secondsRemain);
checkoutMessage(key, count, jsonResponse);
return jsonResponse.toString();
}
count = 0;
if (exists) {
jedis.del(key);
}
checkoutMessage(key, count, jsonResponse);
return jsonResponse.toString();
}

/**
*
* @param key
* @param count 尝试次数,也可以改为从redis里直接读
* @param jsonResponse
* @return
*/
private void checkoutMessage(String key, int count, JSONResponse jsonResponse) {
if (count == 0) {
jsonResponse.setCode(0);
jsonResponse.addMsg("success", "恭喜,登录成功!");
return;
}
jsonResponse.setCode(1);
if (count >= MAX_TRY_COUNT) {
long pttlSeconds = jedis.pttl(key) / 1000;
long hours = pttlSeconds / 3600;
long sencondsRemain = pttlSeconds - hours * 3600;
long minutes = sencondsRemain / 60;
long seconds = sencondsRemain - minutes * 60;
jsonResponse.addError("login_disabled", "登录超过" + MAX_TRY_COUNT + "次,请" + hours + "小时" + minutes + "分" + seconds + "秒后再试!");
return;
}
jsonResponse.addError("username_or_password_is_wrong", "密码错误,您还有 " + (MAX_TRY_COUNT - count) + " 次机会!");
}
}

编写前端页面

页面很简单,监听表单提交事件,用 ajax 提交表单数据,然后将认证结果显示到 div 中。

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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
<style>
span.error {
color: red;
}
span.msg {
color: green;
}
</style>
</head>
<body>
<form action="" method="post">
<label>用户名</label><input type="text" name="username">
<label>密码</label><input type="text" name="password">
<button type="submit">登录</button>
<div></div>
</form>

<script>
(function($) {
var $ = (selector) => document.querySelector(selector);
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
var response = JSON.parse(this.responseText);
var html = '';
var msgNode = '';
if (response.code != 0) {
msgNode = 'error';
} else {
msgNode = 'msg';
}
for (var key in response[msgNode]) {
html += '<span class="' + msgNode + '">' + response[msgNode][key] + '</span>';
}
$('div').innerHTML = html;
}
}

var ajax = function(formData) {
xhr.open('POST', '/api/auth/login.json', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); // 将请求头设置为表单方式提交
xhr.send(formData);
}
$('form').addEventListener('submit', function(event) {
event.preventDefault();
var formData = '';
for (var elem of ['username', 'password']) {
var value = $('input[name="' + elem + '"]').value;
formData += (elem + '=' + value + '&');
}
ajax(formData);
});
})();
</script>
</body>
</html>

源码

最后上下源码地址:https://github.com/liangzai-cool/loginsecuritybyredis

更新

2016年11月29日 更新,代码优化,增加原子操作,org.xueliang.loginsecuritybyredis.web.controller.api.AuthApi#login 函数作如下优化:

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
    @RequestMapping(value = {"login"}, method = RequestMethod.POST)
public String login(@RequestParam("username") String username, @RequestParam("password") String password) {
JSONResponse jsonResponse = new JSONResponse();
String key = username;
String countString = jedis.get(key);
boolean exists = countString != null;
int count = exists ? Integer.parseInt(countString) : 0;
if (count >= MAX_TRY_COUNT) {
checkoutMessage(key, count, jsonResponse);
return jsonResponse.toString();
}
User user = USER_DATA.get(username + "_" + password);
if (user == null) {
count++;
// int secondsRemain = MAX_DISABLED_SECONDS;
// if (exists && count < 5) {
// secondsRemain = (int)(jedis.pttl(key) / 1000);
// }
// jedis.set(key, count + "");
// jedis.expire(key, secondsRemain);
if (exists) {
jedis.incr(key);
if (count >= MAX_TRY_COUNT) {
jedis.expire(key, MAX_DISABLED_SECONDS);
}
} else {
jedis.set(key, count + "");
jedis.expire(key, MAX_DISABLED_SECONDS);
}
checkoutMessage(key, count, jsonResponse);
return jsonResponse.toString();
}
count = 0;
if (exists) {
jedis.del(key);
}
checkoutMessage(key, count, jsonResponse);
return jsonResponse.toString();
}

相关文章

  • Spring MVC + Security 4 初体验(Java配置版)
  • Java程序通过代理访问网络

解决CenOS 7下启动ActiveMQ时报错

发表于 2016-10-11 | 更新于 2018-12-01 | | 阅读次数: | 阅读人数:

基于 CentOS 7,ActiveMQ 5.9.1

问题重现

在 CentOS 7 下安装好ActiveMQ后,执行 /usr/local/apache-activemq-5.9.1/bin/activemq start 启动 ActiveMQ,显示:

INFO: Using default configuration
(you can configure options in one of these file: /etc/default/activemq /home/xueliang/.activemqrc)

INFO: Invoke the following command to create a configuration file
/usr/local/apache-activemq-5.9.1/bin/activemq setup [ /etc/default/activemq | /home/xueliang/.activemqrc ]

INFO: Using java ‘/usr/local/java/jdk1.8.0_101/bin/java’
INFO: Starting - inspect logfiles specified in logging.properties and log4j.properties to get details
INFO: pidfile created : ‘/usr/local/apache-activemq-5.9.1/data/activemq-server2.pid’ (pid ‘24484’)

从提示信息看,似乎启动成功,但根据提示信息中的 pid 查找进程时,却发现并无此进程:

1
ps -eLf | grep 24484

结果:

[xueliang@server2 ~]\$ ps -eLf | grep 24484
xueliang 24520 23462 24520 0 1 00:52 pts/2 00:00:00 grep –color=auto 24484
[xueliang@server2 ~]\$

问题原因

一个好的软件,总能在错误发生时告诉你如何解决。
执行 cat /usr/local/apache-activemq-5.9.1/data/activemq.log 看一下日志信息:

2016-10-12 02:04:09,053 | INFO | Refreshing org.apache.activemq.xbean.XBeanBrokerFactory\$1@13c78c0b: startup date [Wed Oct 12 02:04:09 CST 2016]; root of context hierarchy | org.apache.activemq.xbean.XBeanBrokerFactory\$1 | main
2016-10-12 02:04:10,498 | INFO | PListStore:[/usr/local/apache-activemq-5.9.1/data/localhost/tmp_storage] started | org.apache.activemq.store.kahadb.plist.PListStoreImpl | main
2016-10-12 02:04:10,512 | INFO | Using Persistence Adapter: KahaDBPersistenceAdapter[/usr/local/apache-activemq-5.9.1/data/kahadb] | org.apache.activemq.broker.BrokerService | main
2016-10-12 02:04:10,683 | INFO | KahaDB is version 5 | org.apache.activemq.store.kahadb.MessageDatabase | main
2016-10-12 02:04:10,746 | INFO | Recovering from the journal … | org.apache.activemq.store.kahadb.MessageDatabase | main
2016-10-12 02:04:10,757 | INFO | Recovery replayed 332 operations from the journal in 0.071 seconds. | org.apache.activemq.store.kahadb.MessageDatabase | main
2016-10-12 02:04:10,955 | INFO | Apache ActiveMQ 5.9.1 (localhost, ID:server2-35685-1476209050769-0:1) is starting | org.apache.activemq.broker.BrokerService | main
2016-10-12 02:04:10,969 | ERROR | Failed to start Apache ActiveMQ ([localhost, ID:server2-35685-1476209050769-0:1], java.io.IOException: Transport Connector could not be registered in JMX: Failed to bind to server socket: tcp://0.0.0.0:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600 due to: java.net.BindException: Address already in use) | org.apache.activemq.broker.BrokerService | main
2016-10-12 02:04:10,970 | INFO | Apache ActiveMQ 5.9.1 (localhost, ID:server2-35685-1476209050769-0:1) is shutting down | org.apache.activemq.broker.BrokerService | main
2016-10-12 02:04:10,970 | INFO | Connector openwire stopped | org.apache.activemq.broker.TransportConnector | main
2016-10-12 02:04:10,970 | INFO | Connector amqp stopped | org.apache.activemq.broker.TransportConnector | main
2016-10-12 02:04:10,970 | INFO | Connector stomp stopped | org.apache.activemq.broker.TransportConnector | main
2016-10-12 02:04:10,970 | INFO | Connector mqtt stopped | org.apache.activemq.broker.TransportConnector | main
2016-10-12 02:04:10,970 | INFO | Connector ws stopped | org.apache.activemq.broker.TransportConnector | main
2016-10-12 02:04:10,973 | INFO | PListStore:[/usr/local/apache-activemq-5.9.1/data/localhost/tmp_storage] stopped | org.apache.activemq.store.kahadb.plist.PListStoreImpl | main
2016-10-12 02:04:10,973 | INFO | Stopping async queue tasks | org.apache.activemq.store.kahadb.KahaDBStore | main
2016-10-12 02:04:10,973 | INFO | Stopping async topic tasks | org.apache.activemq.store.kahadb.KahaDBStore | main
2016-10-12 02:04:10,974 | INFO | Stopped KahaDB | org.apache.activemq.store.kahadb.KahaDBStore | main
2016-10-12 02:04:11,193 | INFO | Apache ActiveMQ 5.9.1 (localhost, ID:server2-35685-1476209050769-0:1) uptime 0.691 seconds | org.apache.activemq.broker.BrokerService | main
2016-10-12 02:04:11,193 | INFO | Apache ActiveMQ 5.9.1 (localhost, ID:server2-35685-1476209050769-0:1) is shutdown | org.apache.activemq.broker.BrokerService | main
2016-10-12 02:04:11,193 | INFO | Closing org.apache.activemq.xbean.XBeanBrokerFactory\$1@13c78c0b: startup date [Wed Oct 12 02:04:09 CST 2016]; root of context hierarchy | org.apache.activemq.xbean.XBeanBrokerFactory\$1 | main
2016-10-12 02:04:11,194 | WARN | Exception thrown from LifecycleProcessor on context close | org.apache.activemq.xbean.XBeanBrokerFactory\$1 | main
java.lang.IllegalStateException: LifecycleProcessor not initialized - call ‘refresh’ before invoking lifecycle methods via the context: org.apache.activemq.xbean.XBeanBrokerFactory\$1@13c78c0b: startup date [Wed Oct 12 02:04:09 CST 2016]; root of context hierarchy
at org.springframework.context.support.AbstractApplicationContext.getLifecycleProcessor(AbstractApplicationContext.java:360)
at org.springframework.context.support.AbstractApplicationContext.doClose(AbstractApplicationContext.java:1057)
at org.springframework.context.support.AbstractApplicationContext.close(AbstractApplicationContext.java:1010)
at org.apache.activemq.hooks.SpringContextHook.run(SpringContextHook.java:30)
at org.apache.activemq.broker.BrokerService.stop(BrokerService.java:809)
at org.apache.activemq.xbean.XBeanBrokerService.stop(XBeanBrokerService.java:122)
at org.apache.activemq.broker.BrokerService.start(BrokerService.java:601)
at org.apache.activemq.xbean.XBeanBrokerService.afterPropertiesSet(XBeanBrokerService.java:73)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeCustomInitMethod(AbstractAutowireCapableBeanFactory.java:1638)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1579)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1509)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:521)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:458)
at org.springframework.beans.factory.support.AbstractBeanFactory\$1.getObject(AbstractBeanFactory.java:296)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:293)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:628)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:932)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:479)
at org.apache.xbean.spring.context.ResourceXmlApplicationContext.(ResourceXmlApplicationContext.java:64)
at org.apache.xbean.spring.context.ResourceXmlApplicationContext.(ResourceXmlApplicationContext.java:52)
at org.apache.activemq.xbean.XBeanBrokerFactory\$1.(XBeanBrokerFactory.java:104)
at org.apache.activemq.xbean.XBeanBrokerFactory.createApplicationContext(XBeanBrokerFactory.java:104)
at org.apache.activemq.xbean.XBeanBrokerFactory.createBroker(XBeanBrokerFactory.java:67)
at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:71)
at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:54)
at org.apache.activemq.console.command.StartCommand.runTask(StartCommand.java:87)
at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:57)
at org.apache.activemq.console.command.ShellCommand.runTask(ShellCommand.java:150)
at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:57)
at org.apache.activemq.console.command.ShellCommand.main(ShellCommand.java:104)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.activemq.console.Main.runTaskClass(Main.java:262)
at org.apache.activemq.console.Main.main(Main.java:115)
2016-10-12 02:04:30,534 | INFO | Refreshing org.apache.activemq.xbean.XBeanBrokerFactory\$1@13c78c0b: startup date [Wed Oct 12 02:04:30 CST 2016]; root of context hierarchy | org.apache.activemq.xbean.XBeanBrokerFactory\$1 | main
2016-10-12 02:04:31,297 | INFO | PListStore:[/usr/local/apache-activemq-5.9.1/data/localhost/tmp_storage] started | org.apache.activemq.store.kahadb.plist.PListStoreImpl | main
2016-10-12 02:04:31,311 | INFO | Using Persistence Adapter: KahaDBPersistenceAdapter[/usr/local/apache-activemq-5.9.1/data/kahadb] | org.apache.activemq.broker.BrokerService | main
2016-10-12 02:04:31,472 | INFO | KahaDB is version 5 | org.apache.activemq.store.kahadb.MessageDatabase | main
2016-10-12 02:04:31,530 | INFO | Recovering from the journal … | org.apache.activemq.store.kahadb.MessageDatabase | main
2016-10-12 02:04:31,543 | INFO | Recovery replayed 334 operations from the journal in 0.068 seconds. | org.apache.activemq.store.kahadb.MessageDatabase | main
2016-10-12 02:04:31,680 | INFO | Apache ActiveMQ 5.9.1 (localhost, ID:server2-52293-1476209071560-0:1) is starting | org.apache.activemq.broker.BrokerService | main
2016-10-12 02:04:31,695 | ERROR | Failed to start Apache ActiveMQ ([localhost, ID:server2-52293-1476209071560-0:1], java.io.IOException: Transport Connector could not be registered in JMX: Failed to bind to server socket: tcp://0.0.0.0:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600 due to: java.net.BindException: Address already in use) | org.apache.activemq.broker.BrokerService | main
2016-10-12 02:04:31,695 | INFO | Apache ActiveMQ 5.9.1 (localhost, ID:server2-52293-1476209071560-0:1) is shutting down | org.apache.activemq.broker.BrokerService | main
2016-10-12 02:04:31,696 | INFO | Connector openwire stopped | org.apache.activemq.broker.TransportConnector | main
2016-10-12 02:04:31,696 | INFO | Connector amqp stopped | org.apache.activemq.broker.TransportConnector | main
2016-10-12 02:04:31,696 | INFO | Connector stomp stopped | org.apache.activemq.broker.TransportConnector | main
2016-10-12 02:04:31,696 | INFO | Connector mqtt stopped | org.apache.activemq.broker.TransportConnector | main
2016-10-12 02:04:31,696 | INFO | Connector ws stopped | org.apache.activemq.broker.TransportConnector | main
2016-10-12 02:04:31,699 | INFO | PListStore:[/usr/local/apache-activemq-5.9.1/data/localhost/tmp_storage] stopped | org.apache.activemq.store.kahadb.plist.PListStoreImpl | main
2016-10-12 02:04:31,699 | INFO | Stopping async queue tasks | org.apache.activemq.store.kahadb.KahaDBStore | main
2016-10-12 02:04:31,699 | INFO | Stopping async topic tasks | org.apache.activemq.store.kahadb.KahaDBStore | main
2016-10-12 02:04:31,700 | INFO | Stopped KahaDB | org.apache.activemq.store.kahadb.KahaDBStore | main
2016-10-12 02:04:31,984 | INFO | Apache ActiveMQ 5.9.1 (localhost, ID:server2-52293-1476209071560-0:1) uptime 0.682 seconds | org.apache.activemq.broker.BrokerService | main
2016-10-12 02:04:31,984 | INFO | Apache ActiveMQ 5.9.1 (localhost, ID:server2-52293-1476209071560-0:1) is shutdown | org.apache.activemq.broker.BrokerService | main
2016-10-12 02:04:31,984 | INFO | Closing org.apache.activemq.xbean.XBeanBrokerFactory\$1@13c78c0b: startup date [Wed Oct 12 02:04:30 CST 2016]; root of context hierarchy | org.apache.activemq.xbean.XBeanBrokerFactory\$1 | main
2016-10-12 02:04:31,985 | WARN | Exception thrown from LifecycleProcessor on context close | org.apache.activemq.xbean.XBeanBrokerFactory\$1 | main
java.lang.IllegalStateException: LifecycleProcessor not initialized - call ‘refresh’ before invoking lifecycle methods via the context: org.apache.activemq.xbean.XBeanBrokerFactory\$1@13c78c0b: startup date [Wed Oct 12 02:04:30 CST 2016]; root of context hierarchy
at org.springframework.context.support.AbstractApplicationContext.getLifecycleProcessor(AbstractApplicationContext.java:360)
at org.springframework.context.support.AbstractApplicationContext.doClose(AbstractApplicationContext.java:1057)
at org.springframework.context.support.AbstractApplicationContext.close(AbstractApplicationContext.java:1010)
at org.apache.activemq.hooks.SpringContextHook.run(SpringContextHook.java:30)
at org.apache.activemq.broker.BrokerService.stop(BrokerService.java:809)
at org.apache.activemq.xbean.XBeanBrokerService.stop(XBeanBrokerService.java:122)
at org.apache.activemq.broker.BrokerService.start(BrokerService.java:601)
at org.apache.activemq.xbean.XBeanBrokerService.afterPropertiesSet(XBeanBrokerService.java:73)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeCustomInitMethod(AbstractAutowireCapableBeanFactory.java:1638)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1579)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1509)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:521)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:458)
at org.springframework.beans.factory.support.AbstractBeanFactory\$1.getObject(AbstractBeanFactory.java:296)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:293)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:628)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:932)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:479)
at org.apache.xbean.spring.context.ResourceXmlApplicationContext.(ResourceXmlApplicationContext.java:64)
at org.apache.xbean.spring.context.ResourceXmlApplicationContext.(ResourceXmlApplicationContext.java:52)
at org.apache.activemq.xbean.XBeanBrokerFactory\$1.(XBeanBrokerFactory.java:104)
at org.apache.activemq.xbean.XBeanBrokerFactory.createApplicationContext(XBeanBrokerFactory.java:104)
at org.apache.activemq.xbean.XBeanBrokerFactory.createBroker(XBeanBrokerFactory.java:67)
at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:71)
at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:54)
at org.apache.activemq.console.command.StartCommand.runTask(StartCommand.java:87)
at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:57)
at org.apache.activemq.console.command.ShellCommand.runTask(ShellCommand.java:150)
at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:57)
at org.apache.activemq.console.command.ShellCommand.main(ShellCommand.java:104)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.activemq.console.Main.runTaskClass(Main.java:262)
at org.apache.activemq.console.Main.main(Main.java:115)
2016-10-12 02:07:32,656 | INFO | Refreshing org.apache.activemq.xbean.XBeanBrokerFactory\$1@13c78c0b: startup date [Wed Oct 12 02:07:32 CST 2016]; root of context hierarchy | org.apache.activemq.xbean.XBeanBrokerFactory\$1 | main
2016-10-12 02:07:33,326 | INFO | PListStore:[/usr/local/apache-activemq-5.9.1/data/localhost/tmp_storage] started | org.apache.activemq.store.kahadb.plist.PListStoreImpl | main
2016-10-12 02:07:33,343 | INFO | Using Persistence Adapter: KahaDBPersistenceAdapter[/usr/local/apache-activemq-5.9.1/data/kahadb] | org.apache.activemq.broker.BrokerService | main
2016-10-12 02:07:33,528 | INFO | KahaDB is version 5 | org.apache.activemq.store.kahadb.MessageDatabase | main
2016-10-12 02:07:33,587 | INFO | Recovering from the journal … | org.apache.activemq.store.kahadb.MessageDatabase | main
2016-10-12 02:07:33,602 | INFO | Recovery replayed 336 operations from the journal in 0.071 seconds. | org.apache.activemq.store.kahadb.MessageDatabase | main
2016-10-12 02:07:33,740 | INFO | Apache ActiveMQ 5.9.1 (localhost, ID:server2-53098-1476209253622-0:1) is starting | org.apache.activemq.broker.BrokerService | main
2016-10-12 02:07:33,749 | ERROR | Failed to start Apache ActiveMQ ([localhost, ID:server2-53098-1476209253622-0:1], java.io.IOException: Transport Connector could not be registered in JMX: Failed to bind to server socket: tcp://0.0.0.0:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600 due to: java.net.BindException: Address already in use) | org.apache.activemq.broker.BrokerService | main
2016-10-12 02:07:33,750 | INFO | Apache ActiveMQ 5.9.1 (localhost, ID:server2-53098-1476209253622-0:1) is shutting down | org.apache.activemq.broker.BrokerService | main
2016-10-12 02:07:33,750 | INFO | Connector openwire stopped | org.apache.activemq.broker.TransportConnector | main
2016-10-12 02:07:33,750 | INFO | Connector amqp stopped | org.apache.activemq.broker.TransportConnector | main
2016-10-12 02:07:33,750 | INFO | Connector stomp stopped | org.apache.activemq.broker.TransportConnector | main
2016-10-12 02:07:33,750 | INFO | Connector mqtt stopped | org.apache.activemq.broker.TransportConnector | main
2016-10-12 02:07:33,750 | INFO | Connector ws stopped | org.apache.activemq.broker.TransportConnector | main
2016-10-12 02:07:33,752 | INFO | PListStore:[/usr/local/apache-activemq-5.9.1/data/localhost/tmp_storage] stopped | org.apache.activemq.store.kahadb.plist.PListStoreImpl | main
2016-10-12 02:07:33,753 | INFO | Stopping async queue tasks | org.apache.activemq.store.kahadb.KahaDBStore | main
2016-10-12 02:07:33,753 | INFO | Stopping async topic tasks | org.apache.activemq.store.kahadb.KahaDBStore | main
2016-10-12 02:07:33,753 | INFO | Stopped KahaDB | org.apache.activemq.store.kahadb.KahaDBStore | main
2016-10-12 02:07:34,039 | INFO | Apache ActiveMQ 5.9.1 (localhost, ID:server2-53098-1476209253622-0:1) uptime 0.710 seconds | org.apache.activemq.broker.BrokerService | main
2016-10-12 02:07:34,039 | INFO | Apache ActiveMQ 5.9.1 (localhost, ID:server2-53098-1476209253622-0:1) is shutdown | org.apache.activemq.broker.BrokerService | main
2016-10-12 02:07:34,040 | INFO | Closing org.apache.activemq.xbean.XBeanBrokerFactory\$1@13c78c0b: startup date [Wed Oct 12 02:07:32 CST 2016]; root of context hierarchy | org.apache.activemq.xbean.XBeanBrokerFactory\$1 | main
2016-10-12 02:07:34,041 | WARN | Exception thrown from LifecycleProcessor on context close | org.apache.activemq.xbean.XBeanBrokerFactory\$1 | main
java.lang.IllegalStateException: LifecycleProcessor not initialized - call ‘refresh’ before invoking lifecycle methods via the context: org.apache.activemq.xbean.XBeanBrokerFactory\$1@13c78c0b: startup date [Wed Oct 12 02:07:32 CST 2016]; root of context hierarchy
at org.springframework.context.support.AbstractApplicationContext.getLifecycleProcessor(AbstractApplicationContext.java:360)
at org.springframework.context.support.AbstractApplicationContext.doClose(AbstractApplicationContext.java:1057)
at org.springframework.context.support.AbstractApplicationContext.close(AbstractApplicationContext.java:1010)
at org.apache.activemq.hooks.SpringContextHook.run(SpringContextHook.java:30)
at org.apache.activemq.broker.BrokerService.stop(BrokerService.java:809)
at org.apache.activemq.xbean.XBeanBrokerService.stop(XBeanBrokerService.java:122)
at org.apache.activemq.broker.BrokerService.start(BrokerService.java:601)
at org.apache.activemq.xbean.XBeanBrokerService.afterPropertiesSet(XBeanBrokerService.java:73)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeCustomInitMethod(AbstractAutowireCapableBeanFactory.java:1638)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1579)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1509)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:521)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:458)
at org.springframework.beans.factory.support.AbstractBeanFactory\$1.getObject(AbstractBeanFactory.java:296)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:293)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:628)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:932)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:479)
at org.apache.xbean.spring.context.ResourceXmlApplicationContext.(ResourceXmlApplicationContext.java:64)
at org.apache.xbean.spring.context.ResourceXmlApplicationContext.(ResourceXmlApplicationContext.java:52)
at org.apache.activemq.xbean.XBeanBrokerFactory\$1.(XBeanBrokerFactory.java:104)
at org.apache.activemq.xbean.XBeanBrokerFactory.createApplicationContext(XBeanBrokerFactory.java:104)
at org.apache.activemq.xbean.XBeanBrokerFactory.createBroker(XBeanBrokerFactory.java:67)
at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:71)
at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:54)
at org.apache.activemq.console.command.StartCommand.runTask(StartCommand.java:87)
at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:57)
at org.apache.activemq.console.command.ShellCommand.runTask(ShellCommand.java:150)
at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:57)
at org.apache.activemq.console.command.ShellCommand.main(ShellCommand.java:104)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.activemq.console.Main.runTaskClass(Main.java:262)
at org.apache.activemq.console.Main.main(Main.java:115)

刚开始把精力留在了最后一个 WARN 处,即:

2016-10-12 01:08:54,001 | WARN | Exception thrown from LifecycleProcessor on context close | org.apache.activemq.xbean.XBeanBrokerFactory\$1 | main
java.lang.IllegalStateException: LifecycleProcessor not initialized - call ‘refresh’ before invoking lifecycle methods via the context: org.apache.activemq.xbean.XBeanBrokerFactory\$1@13c78c0b: startup date [Wed Oct 12 01:08:52 CST 2016]; root of context hierarchy

Google 了很久,问题并没有解决,其实问题在上面第一个 ERROR 处,即:

2016-10-12 01:08:53,602 | ERROR | Failed to start Apache ActiveMQ ([localhost, ID:server2-43498-1476205733474-0:1], java.io.IOException: Transport Connector could not be registered in JMX: Failed to bind to server socket: tcp://0.0.0.0:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600 due to: java.net.BindException: Address already in use) | org.apache.activemq.broker.BrokerService | main

从提示信息中的 due to: java.net.BindException: Address already in use 可以看到是端口 61616 被占用了。

解决方案

既然问题找到了,也就知道怎么解决。
使用 netstat -anp | grep 61616 查看是哪个程序占用了 61616 端口:

[xueliang@server2 ~]\$ netstat -anp | grep 61616
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp6 0 0 :::61616 :::* LISTEN 10738/java
[xueliang@server2 ~]\$

可以看出 PID 是 10738,可以确定的是该进程可以直接执行以下命令终止掉:

1
kill 10738

再次执行 netstat -anp | grep 61616 可以看到刚才占用端口程序已经终止运行了。
再次启动 ActiveMQ,并查看日志信息:

[xueliang@server2 ~]\$ cat /usr/local/apache-activemq-5.9.1/data/activemq.log
2016-10-12 01:35:56,610 | INFO | Refreshing org.apache.activemq.xbean.XBeanBrokerFactory\$1@13c78c0b: startup date [Wed Oct 12 01:35:56 CST 2016]; root of context hierarchy | org.apache.activemq.xbean.XBeanBrokerFactory\$1 | main
2016-10-12 01:35:57,423 | INFO | PListStore:[/usr/local/apache-activemq-5.9.1/data/localhost/tmp_storage] started | org.apache.activemq.store.kahadb.plist.PListStoreImpl | main
2016-10-12 01:35:57,436 | INFO | Using Persistence Adapter: KahaDBPersistenceAdapter[/usr/local/apache-activemq-5.9.1/data/kahadb] | org.apache.activemq.broker.BrokerService | main
2016-10-12 01:35:57,593 | INFO | KahaDB is version 5 | org.apache.activemq.store.kahadb.MessageDatabase | main
2016-10-12 01:35:57,716 | INFO | Recovering from the journal … | org.apache.activemq.store.kahadb.MessageDatabase | main
2016-10-12 01:35:57,717 | INFO | Recovery replayed 1 operations from the journal in 0.121 seconds. | org.apache.activemq.store.kahadb.MessageDatabase | main
2016-10-12 01:35:57,844 | INFO | Apache ActiveMQ 5.9.1 (localhost, ID:server2-47815-1476207357729-0:1) is starting | org.apache.activemq.broker.BrokerService | main
2016-10-12 01:35:57,855 | INFO | Listening for connections at: tcp://server2:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600 | org.apache.activemq.transport.TransportServerThreadSupport | main
2016-10-12 01:35:57,855 | INFO | Connector openwire started | org.apache.activemq.broker.TransportConnector | main
2016-10-12 01:35:57,857 | INFO | Listening for connections at: amqp://server2:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600 | org.apache.activemq.transport.TransportServerThreadSupport | main
2016-10-12 01:35:57,857 | INFO | Connector amqp started | org.apache.activemq.broker.TransportConnector | main
2016-10-12 01:35:57,859 | INFO | Listening for connections at: stomp://server2:61613?maximumConnections=1000&wireFormat.maxFrameSize=104857600 | org.apache.activemq.transport.TransportServerThreadSupport | main
2016-10-12 01:35:57,862 | INFO | Connector stomp started | org.apache.activemq.broker.TransportConnector | main
2016-10-12 01:35:57,864 | INFO | Listening for connections at: mqtt://server2:1883?maximumConnections=1000&wireFormat.maxFrameSize=104857600 | org.apache.activemq.transport.TransportServerThreadSupport | main
2016-10-12 01:35:57,864 | INFO | Connector mqtt started | org.apache.activemq.broker.TransportConnector | main
2016-10-12 01:35:58,018 | INFO | Listening for connections at ws://server2:61614?maximumConnections=1000&wireFormat.maxFrameSize=104857600 | org.apache.activemq.transport.ws.WSTransportServer | main
2016-10-12 01:35:58,019 | INFO | Connector ws started | org.apache.activemq.broker.TransportConnector | main
2016-10-12 01:35:58,048 | INFO | Apache ActiveMQ 5.9.1 (localhost, ID:server2-47815-1476207357729-0:1) started | org.apache.activemq.broker.BrokerService | main
2016-10-12 01:35:58,048 | INFO | For help or more information please see: http://activemq.apache.org | org.apache.activemq.broker.BrokerService | main
2016-10-12 01:35:58,536 | INFO | ActiveMQ WebConsole available at http://localhost:8161/ | org.apache.activemq.web.WebConsoleStarter | main
2016-10-12 01:35:58,741 | INFO | Initializing Spring FrameworkServlet ‘dispatcher’ | /admin | main
2016-10-12 01:36:01,077 | INFO | jolokia-agent: No access restrictor found at classpath:/jolokia-access.xml, access to all MBeans is allowed | /api | main
[xueliang@server2 ~]\$

可以看到已经没有了错误信息,ActiveMQ 正常启动。

其他可能导致启动失败的原因

在查找问题原因的过程中,发现一些其他可能导致 ActiveMQ 启动失败的原因:

  1. 主机名包含下划线,相关链接:http://stackoverflow.com/a/21255931/5122380
  2. 将 conf/activemq.xml 文件中 transportConnectors/transportConnector uri 属性中的 0.0.0.0 替换成你主机的域名,或者 127.0.0.1 ,形如:

    <transportConnectors>

        &lt;!-- DOS protection, limit concurrent connections to 1000 and frame size to 100MB --&gt;
        &lt;transportConnector name="openwire" uri="tcp://127.0.0.1:61616?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/&gt;
    &lt;/transportConnectors&gt;
    

相关链接:http://stackoverflow.com/a/25039610/5122380

WinSCP 中普通用户以 root 身份登录 Linux

发表于 2016-09-25 | 更新于 2018-12-01 | | 阅读次数: | 阅读人数:

版本说明:Windows 10,CentOS 7,WinSCP 5.7.7 (Build 6257)

问题背景

使用 WinSCP 登录 CentOS 上传文件,使用的是普通用户,且已加入 sudoers ,向 /usr/local 目录上传文件时,提示没有权限。

解决方案

  1. 首先确保你的目标主机的 sshd 服务正在运行
  2. 用来在 WinSCP 登录的普通用户已加入 sudoers
  3. 获取 sftp-server 的位置

    1. 从 /etc/ssh/sshd_config 文件中获取:

      1
      sudo cat /etc/ssh/sshd_config | grep sftp

      结果:

      Subsystem sftp /usr/libexec/openssh/sftp-server

      1. 直接查找:

        1
        2
        sudo updatedb
        locate sftp-server

        结果:

        /usr/libexec/openssh/sftp-server
        /usr/share/man/man8/sftp-server.8.gz

  4. 以管理员方式运行 WinSCP,打开对应帐户的Advanced Site Settings 对话框中,选中 Environment → SFTP 节点,在右边的 Protocol options - SFTP server 输入框中,填入 sudo -s /usr/libexec/openssh/sftp-server ,这里的 /usr/libexec/openssh/sftp-server 换成在你的系统中,由第 3 步得到的路径,之后保存。

  5. 在 CentOS 中执行 sudo visudo 以编辑 /etc/sudoers 文件

    1. 找到需要在 WinSCP 登录的账户名配置信息,大概在第 98 行,将:myloginname ALL=(ALL) ALL 改为: myloginname ALL=(ALL) NOPASSWD: ALL 。这一步的目的是切换为 root 角色时不需要输入密码,因为 WinSCP 只能执行不需要请求用户输入其他信息(比如:密码等)的命令。
      切记:记得使用完后,将这一行的内容恢复到修改前的样子!
    2. 找到 Defaults requiretty 这一行,在前面加一个 # 号注释掉这一行,这一步的目的是关闭控制终端。

到此,即可以 root 角色登录系统啦!

解决方案优化

也许你担心由于上述解决方案的第 5 步,在切换为 root 时不需要输入密码,会造成系统不安全。
确实是这样,如果你系统安全要求较高,我建议你新建一个帐户,专门用于 WinSCP 中以 root 角色登录。

MySQL 中 AUTO_INCREMENT 的“坑”

发表于 2016-09-24 | 更新于 2018-12-01 | | 阅读次数: | 阅读人数:

基于 MySQL 5.6

背景

最近在玩 MySQL 双主复制架构,表里的主键使用自增ID,为了避免两台主库生成的主键冲突,遂两台主库分别配置如下:
server 1 的 my.cnf :

1
2
auto_increment_increment = 2
auto_increment_offset = 1

server 2 的 my.cnf :

1
2
auto_increment_increment = 2
auto_increment_offset = 2

按照这个配置,本以为 server 1 和 server 2 生成序列分别是 1 ,3 ,5 ··· 和 2 , 4, 6 ··· 这样的序列,但事实上并不完全是这样,下面来做个试验。

重现

基于以上配置,在 server 1 上建表:

1
2
3
4
5
6
CREATE TABLE `test`.`table_name` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NULL,
PRIMARY KEY (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8mb4;

因为我配置了双主同步,所以此表将被同步到 server 2 上。

执行如下添加语句初始化数据:

1
2
3
INSERT INTO `test`.`table_name` (`name`) VALUES ('myname0');
INSERT INTO `test`.`table_name` (`name`) VALUES ('myname1');
INSERT INTO `test`.`table_name` (`name`) VALUES ('myname2');

数据将被同步到 server 2 上。

在 server 1 上查询此表,可以看到刚才插入的数据:
id | name
– | –
1 | myname0
3 | myname1
5 | myname2
结果如我们所料,id 列呈奇数自增。在 server 2 上查询的结果和上面一样。

接着,在 server 2 上向此表再添加几条数据:

1
2
3
INSERT INTO `test`.`table_name` (`name`) VALUES ('myname3');
INSERT INTO `test`.`table_name` (`name`) VALUES ('myname4');
INSERT INTO `test`.`table_name` (`name`) VALUES ('myname5');

同样数据,将被同步到 server 1 上。

查询此表,得到的结果:
id | name
– | –
1 | myname0
3 | myname1
5 | myname2
6 | myname3
8 | myname4
10 | myname5

问题出来了,server 2 分配的序列并不像我们之前期望的那样,从 2 开始的连续偶数,而是跳过 2 和 4,直接从 6 开始。

解决

研究了很久,翻看 MySQL 官网文档,没有提到会是这样子,Goole 了两天,也都说,自增ID分别是 1、2、3 和 2 、 4 、 6 ,并没有对此情况做明确说明。直到我看到 这位大神的回答。
具体的我就不重复了。

总结

总结一下:

  1. AUTO_INCREMENT 所在的列,必须为整数型数据列
  2. AUTO_INCREMENT 所在的列,不能为空
  3. AUTO_INCREMENT 所在的列,必须有唯一索引
  4. AUTO_INCREMENT 所在的列,值必须大于0
  5. AUTO_INCREMENT 所在的列,最大值,受其数据类型及是否为 无符号(Unsigned) 限制,若使用的为 TINYINT(4) 且 为无符号的,则最大值为 255,若继续插入数据,则该列的值保持最大值不变,
  6. AUTO_INCREMENT 所在的列,若向其中插入的值,大于所在表当前的 AUTO_INCREMENT 值,则会更新表 AUTO_INCREMENT 值至 current_max_value - (current_max_value - auto_increment_offset) % auto_increment_increment + auto_increment_increment ,即该列的下一个序列值

安装 Windows 10 + Centos 7 双系统共存

发表于 2016-09-19 | 更新于 2018-12-01 | | 阅读次数: | 阅读人数:

第一步 准备工作

分区

给CentOS 系统单独创建一个分区,具体看空闲硬盘的大小决定。

  • 右键桌面上 [我的电脑] 或者 [此电脑] 图标,选择 [管理] → [计算机管理] → [存储] - [磁盘管理]
  • 选择一个空闲空间较大的分区,右键选择 [压缩卷(H)…]
  • 在弹出的对话框中输入需要分配给 CentOS 系统的空间大小,以 MB 为单位
  • 输入完毕后,点击对话框右下角的 [压缩] 按钮执行压缩即可

注意:压缩出的新分区,不要执行 [新建简单卷],否则稍后 CentOS 不识别,到此步即可

下载 CentOS 7 系统文件

到 CentOS官网 下载 ISO 文件,DVD 版即可。

制作优盘启动盘

同样很简单:

  • 准备一个优盘,备份里面所有你认为不能删的文件,稍后将会格式化此优盘
  • 到 UltraISO官网 下载软件
  • 安装,我基本是用完就卸载,所以,除了安装路径改下,其他全部默认
  • 运行 UltraISO,选择试用,选择主界面菜单栏里的[文件] → [打开],选择你刚下载好的 CentOS 7 镜像
  • 选择菜单栏里的 [启动] → [写入硬盘映像]
  • 在弹出框中确认选中的优盘无误,其他选项保持默认,无需修改,直接点击下方的 [写入] 按钮,执行制作优盘启动盘,稍后确认制作成功的提示信息即可

准备工作到此结束

第二步 从优盘启动安装

调整 BIOS 引导顺序

  • 插入刚刚制作的优盘启动盘,重启电脑,开机阶段按 F2 、 F8 或 F12 进入 BIOS
  • 切换到 Boot 界面,找到优盘启动项,使用 F5 或 F6 调整其顺序至第一位,即从优盘启动
  • 按 F10 保存并退出 BIOS ,即可从优盘引导启动。
  • 稍后进入一个标题为 CentOS 7 的黑白界面:
    图片加载中...

    同时提示 Press Tab for full configuration options on menu items. 和 Automatic boot in 60 seconds...。

  • 默认选中的是 第一项即 Install CentOS 7,按 Tab 键,下方的提示信息将显示为 vmlinuz initrd=initrd.img inst.stage2=hd:LABEL=CentOS\x207x20x86_64 rd.live.check quiet
    图片加载中...

  • 移动光标,删除 LABEL=CentOS\x207x20x86_64 rd.live.check 这部分,并用 linux dd 替换,最终的内容为 vmlinuz initrd=initrd.img linux dd quiet
    图片加载中...

  • 确认无误后回车,可以看到如下界面
    图片加载中...
    这里可以看到,界面下方有4列,分别是 DEVICE 、 TYPE 、 LABEL 和 UUID,LABEL 这一列就是驱动器名称,据此找到你的优盘,并记下对应 DEVICE 列的值,一般是 sdb4 。

  • 强制关闭计算机后再开机,回到刚才倒计时那个界面,依旧按 Tab 键,修改启动参数,这次修改为 vmlinuz initrd=initrd.img inst.stage2=hd:/dev/sdb4 rd.live.check quiet,这里的 sdb4 替换成你刚才记下的自己优盘对应的 DEVICE 列的值。
    图片加载中...

  • 确认无误后回车,稍等片刻就可以看到暖暖的界面啦!
    图片加载中...

开始安装

  • 上面的系统语言根据需要自行选择(为了防止系统自动生成家目录下有 桌面 、 下载 这类中文名路径,切换路径的时候还要修改输入法,比较麻烦,所以这里我保持默认,即 English),这里主要说下第二步的 INSTALLATION SUMMARY。
    图片加载中...

选择需要安装的基础软件

  • 点击 SOFTWARE 下的 SOFTWARE SELECTION 选择一些需要安装的基础软件,这里只选择安装一个桌面软件 GNOME Desktop 就够了,选择完成之后点击左上角的 Done 回到 INSTALLATION SUMMARY 主界面。
    图片加载中...

磁盘分区

  • 点击 SYSTEM 下的 INSTALLATION DESTINATION 选择安装位置,这里可以看到我们之前压缩出来的那个新分区,以及优盘分区。
    选中那个新分区,并选中 Other Storage Options → Partitioning 下的 I will configure partitioning 选项,切记,一定要选中此项,否则整个硬盘的数据都将被删除!!!
    确认无误后,点击左上角的 Done 。
    图片加载中...

  • 这一步对硬盘进行分区,可以点击 Click here to create them automatically 即可让安装程序自动创建分区,非常方便,也可以点击左下角写有 AVAILABLE SPACE 字样的红色区域上方的 + 按钮手动创建分区。
    图片加载中...
    选择手动分区的话,要注意单位,这里我选择自动分区。分区完成并且确认无误后,点击左上角的 Done 结束对硬盘的分区操作。下面我讲下手动分区操作步骤。

    点击 + 按钮,在弹出的对话框中选择新分区挂载的路径,并输入新分区的大小,这里我们设置新分区的大小为 6 GB,并挂载在 根目录 / 下。点击 Add mount point 保存分区信息。
    图片加载中...

    此时,可以看到新分区已经出现在左侧栏里了,但分区的单位并不对,默认刚才输入的单位是 MiB 并显示为 KiB ,这里我们修改为 MiB 即可,点击右下角 Update Settings 保存修改后的信息。
    图片加载中...
    以同样的方式添加 swap 等分区,添加完成后,点击左上角的 Done 按钮,完成对磁盘的分区。如果没有给 /boot 进行分区,会在第一次点击 Done 后收到警告信息。可以点击 Click for details 查看警告原因,然后点击 Close 后再次点击一次 Done ;也可以忽略警告信息,再点一次 Done 。
    图片加载中...
    忽略警告信息,点击两次 Done 之后,可以在弹出的对话框中看到,即将对磁盘进行修改的摘要信息,直接点击 Accept Changes ,开始对硬盘执行分区操作。
    图片加载中...

  • 回到 INSTALLATION SUMMARY 界面,待分区执行完成后, SYSTEM 下的 INSTALLATION DESTINATION 下的黄色感叹号就没有。点击右下角的 Begin Installation 按钮开始安装 CentOS 系统。
    图片加载中...

初始化账户

  • 安装进度界面可以看到 USER SETTINGS 下,ROOT PASSWORD 提示需要给 root 账户设置密码(root 账户默认是没有密码的),USER CREATION 提示创建一个普通用户。下面我们按照提示完成对账户的初始化。
    图片加载中...

  • 点击 USER SETTINGS 下的 ROOT PASSWORD ,给 root 账户设置密码。如果设置的密码强度不够,需要点两次 Done 按钮忽略警告信息,完成对 root 账户的初始化。
    图片加载中...

  • 完成对 root 账户初始化后,可以看到 USER SETTINGS 下 USER CREATION 的黄色感叹号已经消失了,说明初始化 root 账户的密码后,创建普通账户已经变成一个可选项。
    图片加载中...

    但推荐仍创建一个普通账户,以便平时使用,而非平时直接使用 root 账户,这样可以减少使用过程中的误操作,以及 root 账户密码泄露的风险。点击 USER CREATION ,创建一个新的账户,同样 ROOT PASSWORD 一样,如果密码强度不够,请点击两次 Done 按钮。
    图片加载中...

完成安装

  • 约15 ~ 30 分钟后,CentOS 7 的安装就大功告成啦。为了避免重启后再次从优盘启动,可以先拔掉优盘,再点击 Reboot 按钮重启计算机;也可以在重启时进入 BIOS 调整引导顺序,恢复硬盘作为第一引导盘。

    图片加载中...

  • 开机后会看到一个黑白屏的启动菜单,并且有一个默认选项,屏幕下方有一个 5 秒的倒计时,若 5 秒内没有任何操作,将使用默认选中的内核启动系统,也可以使用上下键进行切换并按回车键,即可使用选中的内核启动系统。这里默认即可。

第三步 找回 Windows 10 启动项

可能大家都注意到了,上面重启之后,已经无法进入之前的 Windows 10 系统。是的,CentOS 7 不能在安装过程中设置之后的启动项,不像 Ubuntu 那样,在安装 Ubuntu 的过程中,就可以选择启动项,因此这时候就无法进入 Windows 10 了。
不过,不用担心,因为 Windows 10 系统的引导信息以及数据都是还在的,我们只需要在 CentOS 7 中找回 Windows 10 的引导信息并添加到启动菜单中就可以了。

  • 启动 CentOS 7 并使用事先设定的账户,登录系统。这里我使用的是非 root 账户登录的。

安装 ntfs-3g

  • 右键桌面,打开一个终端。执行以下命令安装 ntfs-3g:
    1
    sudo yum install -y ntfs-3g

之所以需要安装 ntfs-3g ,是因为 Windows 家族专用的文件系统,CentOS 7 默认不能识别,而要想在 CentOS 7 系统中找回 Windows 10 的引导信息,势必要让 CentOS 7 系统识别 Windows 10 的文件系统,故安装此库。

更新 Grub2 启动菜单,找回 Windows 10

  • 执行以下命令即可找回 Windows 10 引导信息:
    1
    sudo grub2-mkconfig -o /boot/grub2/grub.cfg

这一条命令,是让 grub2 自动扫描磁盘中已经安装的所有系统的引导信息,并将其加入到启动菜单中。重启一下计算机,在上面提过的启动菜单界面,是不是可以看到一个有 Windows 字眼的启动项啦?这就是你的 Windows 10!切换到此项,回车,我 Windows 10 又回来啦!

第四步 修复启动菜单界面 Windows 版本显示错误的问题

虽然 Windows 10 已经找回,然而,不尽如人意的是,引导 Windows 10 的菜单项上的 Windows 系统的版本并非“Windows 10”,而是“Windows 7”或 “Windows 8.1”等。
初步推测,此启动项上显示的 Windows 版本号,取决于你从何版本的 Windows 升级到 Windows 10的(若你是从 Windows 7 升级到 Windows 10 的,那此启动项的文字就会显示“Windows 7”字眼,对于从 Windows 8.1 升级也是一样的道理。未考虑直接安装 Windows 10 的情况)。
需要说明的是,这里只是一个文字显示错误,对正常使用没有丝毫影响。但你有强迫症的话,请继续往下看。

修复显示错误

我已经打好了一个 patch,放到了这里了,登录已安装好的 CentOS 7 系统,打开一个终端,并这行以下命令即可:

  • 下载patch

    1
    wget https://gist.githubusercontent.com/liangzai-cool/59f0c29a528305a9aa1c3971aeef604b/raw/316a739240b388ff46ee95d38d2c7a97dcd53cbf/20microsoft-win10.patch
  • 安装patch

    1
    ((sudo cp 20microsoft-win10.patch / && cd / && sudo patch -p0  ) < 20microsoft-win10.patch)

错误原因浅析

  • 启动菜单是执行 Grub2 的命令后,由 Grub2 生成的
  • Grub2 本身并不能识别磁盘中已安装的操作系统,它是依赖 os-prober 这个库来识别的
  • os-prober 本是为 debian 系统编写的,其在 debian 系统下不能识别 Windows 10 的问题已经修复
  • CentOS 下的 os-prober 目前最新版本依旧是 1.58 ,该版本依旧存在此问题,貌似 CentOS 下的 os-prober 无人维护。问题代码位置是 /usr/libexec/os-probes/mounted/20microsoft

Linux下修改系统时区

发表于 2016-08-25 | 更新于 2018-12-01 | | 阅读次数: | 阅读人数:

使用 /etc/localtime 文件修改时区

先查看一下当前的时区,下面这个例子中使用 UTC 即世界统一标准时区。假设你可能需要改为美国西部标准时间,即太平洋时间。

# date
Thu Aug 17 22:59:24 UTC 2016

在某些发行版的 Linux 系统(比如 CentOS)中,系统时区是由 /etc/localtime 文件控制的,所以可以通过修改 /etc/localtime 文件来修改系统时区。

删除 /etc/localtime 文件,

# cd /etc/
# rm localtime

所有的美国时区文件都可以在 /usr/share/zoneinfo/US 目录下找到:

# ls /usr/share/zoneinfo/US/
Alaska Arizona Eastern Hawaii Michigan Pacific
Aleutian Central East-Indiana Indiana-Starke Mountain Samoa

其他国家的时区文件,可以在 /usr/share/zoneinfo/` 找到。

创建一个软连接 /etc/localtime ,指向上述 US 目录中的 Pacific 文件:

# cd /etc
# ln -s /usr/share/zoneinfo/US/Pacific localtime

到此,已经将系统的时区改为美国西部所在的时区:

# date
Thu Aug 17 23:10:14 PDT 2016

使用 /etc/timezone 文件修改时区

在某些发行版的 Linux 系统(比如 Ubuntu)中,系统时区是由 /etc/timezone 文件控制的,所以可以通过修改 /etc/timezone 文件来修改系统时区。

举个例子,你现在的时区可能在美国东部时间(比如:纽约):

# cat /etc/timezone
America/New_York

需要设置到美国太平洋时间(比如:洛杉矶),修改 /etc/timezone 时间:

# vim /etc/timezone
America/Los_Angeles

当然,也可以通过在命令行上修改 TZ 的值来设置时区:

# export TZ=America/Los_Angeles

Git如何检出指定目录或文件

发表于 2016-08-19 | 更新于 2018-12-01 | | 阅读次数: | 阅读人数:

系统版本:Window 10,Git 版本:2.7.1

对于大型 Git 仓库,每次执行 Git 命令,都需要经过漫长的等待,特别是要经常执行的 git status 命令。下面是一个例子…

图片加载中...

从 1.7.0 开始,Git 引入 sparse checkout(稀疏检出) 机制,稀疏检出机制允许只检出指定目录或者文件,这在大型 Git 仓库中,将大幅度缩短 Git 执行命令的时间。

要想只检出指定的目录或文件,需要在 .git/info/sparse-checkout 文件中指定目录或文件的路径,下面将以一个具体例子介绍 如何使用 Git 的 sparse checkout 。

准备远程仓库

初始化一个仓库,目录结构如下图所示:

图片加载中...

根目录下有 2 个子目录,以及一个 LICENSE 文件和 README.md 文件,每个子目录中各有 3 个。
将其推送到Github上新建的一个仓库,地址是 `git@github.com:liangzai-cool/git-sparse-checkout-study.git` 。

为Git配置稀疏检出

换一个目录,再初始化一个 Git 仓库,以便用稀疏检出的方式,检出刚才在 Github 上新建的 git-sparse-checkout-study 仓库:

图片加载中...

使用 git config core.sparseCheckout true 命令开启 Git 稀疏检出模式。然后编辑该仓库目录下的 .git/info/sparse-checkout 文件,指定检出规则。这里只检出 git-sparse-checkout-study 仓库中的 dir1 目录下的所有文件和 根目录下的 README.md 文件:

图片加载中...

检出

添加远程仓库地址,并检出:

图片加载中...

可以看到,Git 只检出了根目录下的 README.md 文件和 dir1 目录。

如果此时需要再检出,根目录下的 dir2 目录,则需要将其加入到 .git/info/sparse-checkout 文件中。参照下图中的方案:

图片加载中...

关闭稀疏检出

和上面检出 dir2 时类似:

图片加载中...

可以看到所有文件都已显示出来了。
注意这里的 echo 命令:

1
echo "/*" > .git/info/sparse-checkout

最后不要忘了配置 Git 的 core.sparseCheckout 为 false 以及移除 .git/info/sparse-checkout 文件。

.git/info/sparse-checkout 中使用和 .gitignore 相同的匹配模式,例如 非匹配 !/dir2/* 以及 /*.java 等。

1234

薛亮

37 日志
45 标签
© 2015 – 2018 薛亮
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Muse v6.5.0