业务上需要实现一个功能,检测到用户不活跃的时候把免费用户的用户计划进行暂停,防止服务器资源浪费。
刚好我在公众号列表看到一篇文章《领导:谁再用Redis做订单到期关闭,立马走人!》
还有这种好事?那我不得马上安排一下,看看能不能喜提N+1。
我大概看了一下,为啥不能用Redis做订单过期关闭?主要就是这东西是不可靠的,如果服务监听了Redis,这时候Key过期了,Redis发了一个事件出来。
正常的时候能收到这个事件。那如果不正常的时候... 绝对收不到。
不正常情况包括,服务突然资源不够,网络突然抽风等等等等。这时候如果订单需要关闭了,但是网络抽风了一下,然后就不关闭超时订单了。
虽然说攻城该为了广大的攻城狮朋友有活干,应该多写些bug。不过这个同学做得这么明显,活该被祭天。这么说来这个特性就只能用在一些可靠性不高的场景了。
画外音:有可靠性不高的场景吗?我写这么久代码没见到过,最多就是不要求实时,然后异步执行。
确实,可靠性不高的场景非常之少,但我运气就是这么好真的遇到了,可以堂而皇之写bug。
那么接下来看看咋实现的,我基于百度编程了一下,说是要继承个接口,然后重写方法,这时候我的键盘突然坏了,只剩下ctrl,c,v 这三个键能用了,那没办法了。
把监听代码搞过来,改改监听的范围。我这里是监听某个库的key删除和过期事件。
@Component
@Slf4j
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
@Value("${spring.redis.database}")
private int db;
@Resource
private List<RedisEventListener> redisEventListeners;
public RedisKeyExpirationListener(RedisMessageListenerContainer container) {
super(container);
}
@Override
protected void doRegister(RedisMessageListenerContainer container) {
// 监听过期事件
container.addMessageListener(this, new PatternTopic("__keyevent@" + db + "__:expired"));
// 监听删除事件
container.addMessageListener(this, new PatternTopic("__keyevent@" + db + "__:del"));
}
@Override
public void onMessage(Message message, byte[] pattern) {
String key = message.toString();
//log.info("Key expired: {}, {}", key, new String(pattern, StandardCharsets.UTF_8));
redisEventListeners.stream()
.filter(listener -> listener.match(key))
.forEach(listener -> listener.handle(key));
}
}
自己定义一个Container 对象,因为我只引入了 spring-data-redis 这个依赖,默认没有这个对象。
@Configuration
public class RedisConfig {
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
把代码弄好之后,测试环境运行没啥问题,提交正式环境。这个时候正式环境不讲武德,直接给我抛了一个异常。
Caused by: org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: ERR unknown command 'CONFIG', with args beginning with: 'GET' 'notify-keyspace-events'
at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:54)
at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:52)
at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41)
at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44)
at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42)
at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:273)
at org.springframework.data.redis.connection.lettuce.LettuceServerCommands.convertLettuceAccessException(LettuceServerCommands.java:571)
at org.springframework.data.redis.connection.lettuce.LettuceServerCommands.getConfig(LettuceServerCommands.java:307)
at org.springframework.data.redis.connection.DefaultedRedisConnection.getConfig(DefaultedRedisConnection.java:1381)
at org.springframework.data.redis.listener.KeyspaceEventMessageListener.init(KeyspaceEventMessageListener.java:89)
at org.springframework.data.redis.listener.KeyspaceEventMessageListener.afterPropertiesSet(KeyspaceEventMessageListener.java:137)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1855)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1792)
... 21 common frames omitted
Caused by: io.lettuce.core.RedisCommandExecutionException: ERR unknown command 'CONFIG', with args beginning with: 'GET' 'notify-keyspace-events'
我不李姐,为什么测试环境没问题正式环境有问题。应该不是代码问题,我用的都是同一套代码。
那有可能是 Redis 问题,测试环境用的是自建的开源 Redis,正式环境用的是 AWS 的 ElasticCache Redis。直接连接上正式环境执行一下Config命令试试。
xxxxxxxxx.cache.amazonaws.com:6379> config
(error) ERR unknown command 'config', with args beginning with:
我还是不李姐,订阅个key过期事件需要执行 Config 命令,明明我用 redis-cli 只需要 subscribe 就足够了啊。难道是 Spring 搞特殊?看看源码,果然有猫腻。
问题找到了,既然 keyspaceNotificationsConfigParameter 有值才会执行 config 相关操作,那我们把 keyspaceNotificationsConfigParameter 设置成空是不是就行了?试试看。RedisKeyExpirationListener 这个类有 set 方法,设置一下看看。
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
// 还有一堆代码被我省略了
public RedisKeyExpirationListener(RedisMessageListenerContainer container) {
super(container);
setKeyspaceNotificationsConfigParameter("");
}
// 这里也有一堆代码被我省略了
}
bingo,直接启动成功,测试也没问题。这时候你肯定在想,解决到这里就告一段落了吧?但我还有 P 要 funk。
虽然上面的解决方案看起来简单,但是中间我还走另外一条路,我看到了这个,然后我就手贱引入了 spring-session。这个库有一个默认的 RedisMessageListenerContainer 所以如果按照我上面的配置就会冲突。
所以,如果你使用了spring-session,你就不要自己实例化 RedisMessageListenerContainer了。同时要加上这个配置,避免又执行 Config 命令(下面这两个二选一,要是我肯定选2。但是为了照顾有些同学的公司按代码行数来计算工作成果,所以附上1)
// 1
@Bean
public ConfigureRedisAction configureRedisAction() {
return ConfigureRedisAction.NO_OP;
}
// 2
spring.session.redis.configure-action=none
以上。