我有一个项目,使用的是spring boot构建,整个系统使用spring security构建了一个登录系统,关于spring boot security 如何使用,这里就不介绍了,这里只描述如何解决我遇到的问题。
背景 : spring boot + spring security 构建的登陆系统,只能满足单机的需求,如果程序部署在集群之上,那么就涉及到了session的共享问题,否则你的用户就会显示一会登陆,一会又没登陆(落到了另外一台机器)。核心思想是通过redis来在集群之间共享用户的session,实现这样的方案有两种方法。
方法一: 通过修改tomcat的配置来实现。不幸的是spring boot的程序,tomcat是内置的(你当然也可以用外置的), 所以先排除这种办法。
方法二: 通过spring session来实现。文档
其实spring boot配置也比较简单,按照官方的文档只需要@EnableRedisHttpSession
,以及在配置文件中配置你的host和password就可以了,配置如下。
spring.redis.host=127.0.0.1
spring.redis.password=xxxx
spring.redis.port=6379
是不是很嗨皮? 本地测试通过了,等你部署到阿里云服务器发现,服务起不来???
刚开始我怀疑是阿里云的redis版本问题,可是他的版本是2.8.14也是2.8版本,理论上是支持的。
问题在于在阿里云提供的redis上无法启动程序,报错如下:
2017-08-17 21:58:48 ERROR SpringApplication.handleRunFailure:827 # Application startup failed org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'enableRedisKeyspaceNotificationsInitializer' defined in class path resource [org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.class]: Invocation of init method failed; nested exception is org.springframework.data.redis.RedisSystemException: Redis exception; nested exception is com.lambdaworks.redis.RedisCommandExecutionException: ERR CONFIG subcommand must be GET at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1578) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:545) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482) at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197) at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:772) at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:839) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:538) at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:118) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:766) at org.springframework.boot.SpringApplication.createAndRefreshContext(SpringApplication.java:361) at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1191) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1180) at com.alibaba.quantum.QuantumPlatform.main(QuantumPlatform.java:22) 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.boot.loader.MainMethodRunner.run(MainMethodRunner.java:54) at java.lang.Thread.run(Thread.java:748) Caused by: org.springframework.data.redis.RedisSystemException: Redis exception; nested exception is com.lambdaworks.redis.RedisCommandExecutionException: ERR CONFIG subcommand must be GET at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:70) at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41) at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:37) at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:37) at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:326) at org.springframework.data.redis.connection.lettuce.LettuceConnection.getConfig(LettuceConnection.java:658) at org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction.getNotifyOptions(ConfigureNotifyKeyspaceEventsAction.java:74) at org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction.configure(ConfigureNotifyKeyspaceEventsAction.java:55) at org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration$EnableRedisKeyspaceNotificationsInitializer.afterPropertiesSet(RedisHttpSessionConfiguration.java:251) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1637) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1574) ... 22 common frames omitted Caused by: com.lambdaworks.redis.RedisCommandExecutionException: ERR CONFIG subcommand must be GET at com.lambdaworks.redis.LettuceFutures.await(LettuceFutures.java:104)
然后我觉得是不是连接出现了问题,所以我又修改换了一种方式来连接,使用Lettuce来连接,依然报错,程序里也没有看到是哪里抛出了ERR CONFIG subcommand must be GET。
2017-08-17 21:58:48 ERROR SpringApplication.handleRunFailure:827 # Application startup failed org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'enableRedisKeyspaceNotificationsInitializer' defined in class path resource [org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.class]: Invocation of init method failed; nested exception is org.springframework.data.redis.RedisSystemException: Redis exception; nested exception is com.lambdaworks.redis.RedisCommandExecutionException: ERR CONFIG subcommand must be GET at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1578) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:545) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482) at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197) at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:772) at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:839) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:538) at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:118) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:766) at org.springframework.boot.SpringApplication.createAndRefreshContext(SpringApplication.java:361) at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1191) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1180) at com.alibaba.quantum.QuantumPlatform.main(QuantumPlatform.java:22) 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.boot.loader.MainMethodRunner.run(MainMethodRunner.java:54) at java.lang.Thread.run(Thread.java:748) Caused by: org.springframework.data.redis.RedisSystemException: Redis exception; nested exception is com.lambdaworks.redis.RedisCommandExecutionException: ERR CONFIG subcommand must be GET at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:70) at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41) at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:37) at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:37) at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:326) at org.springframework.data.redis.connection.lettuce.LettuceConnection.getConfig(LettuceConnection.java:658) at org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction.getNotifyOptions(ConfigureNotifyKeyspaceEventsAction.java:74) at org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction.configure(ConfigureNotifyKeyspaceEventsAction.java:55) at org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration$EnableRedisKeyspaceNotificationsInitializer.afterPropertiesSet(RedisHttpSessionConfiguration.java:251) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1637) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1574) ... 22 common frames omitted Caused by: com.lambdaworks.redis.RedisCommandExecutionException: ERR CONFIG subcommand must be GET at com.lambdaworks.redis.LettuceFutures.await(LettuceFutures.java:104) at com.lambdaworks.redis.LettuceFutures.awaitOrCancel(LettuceFutures.java:77) at com.lambdaworks.redis.FutureSyncInvocationHandler.handleInvocation(FutureSyncInvocationHandler.java:73) at com.google.common.reflect.AbstractInvocationHandler.invoke(AbstractInvocationHandler.java:87) at com.sun.proxy.$Proxy89.configGet(Unknown Source) at org.springframework.data.redis.connection.lettuce.LettuceConnection.getConfig(LettuceConnection.java:656) ... 27 common frames omitted
是为啥呢? 去看了下源代码发现,
org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction 有redis的config set和config get 操作。 于是,通过命令行连上aliyun的redis,果然报错如下:
r-uf6db95eb74a1f64.redis.rds.aliyuncs.com:6379> config set (error) ERR CONFIG subcommand must be GET
再去查看阿里云的官方文档,发现官网说了,不支持config set操作, 看到这里我的眼泪掉下来。好不容易在本地测试通过了,此路居然不通。
到这里我就去查找如何避免spring data redis去调用config set 命令呢? 我去查看
org.springframework.data.redis.connection.jedis.JedisConnection 的代码,发现有if提前返回的? 分别是在 pipline和transaction模式下可以提前返回,后来继续查发现这两个其实仅仅是延迟提交,实际上最后还是要提交。 到这里我几乎就要放弃了,开始想转向用jdbc的方式了,不过每次请求都读一遍数据库实在是不是我愿意看到的。 继续看spring -session文档,customcookie也不能干这事。
public void setConfig(String param, String value) { try { if (isPipelined()) { pipeline(new JedisStatusResult(pipeline.configSet(param, value))); return; } if (isQueueing()) { transaction(new JedisStatusResult(transaction.configSet(param, value))); return; } jedis.configSet(param, value); } catch (Exception ex) { throw convertJedisAccessException(ex); } }
我再看看原始调用的地方是干啥的ConfigureNotifyKeyspaceEventsAction仔细看这个类,发现他是用来测试redis是否支持
notify-keyspace-events这个的,如果不支持就把他开启起来。
Ensures that Redis Keyspace events for Generic commands and Expired events are enabled
那么怎么才能不让这个东西执行了,搜索ConfigureNotifyKeyspaceEventsAction还真的找到了线索,感谢这个人提供的参考建议
http://xxgblog.com/2016/09/29/spring-session-redis/
原来大家都遇到了同样的问题,果真有办法关闭,那么怎么关闭呢? 很简单,在你的autoconfig文件里添加这么一个东西,就可以关闭检查。
@Configuration @EnableRedisHttpSession public class HttpSessionConfig { @Bean public static ConfigureRedisAction configureRedisAction() { return ConfigureRedisAction.NO_OP; } }
添加好了之后,去检查,发现应用正常启动,再到redis去看, 登陆成功之后,session数据成功写入redis,大功告成。
原来我离正确答案这么近,差点我就放弃了。