SpringData-Redis发布订阅自动重连分析

RedisMessageListenerContainer

配置

@Bean
@Autowired
RedisMessageListenerContainer redisContainer(JedisConnectionFactory redisConnectionFactory, RedisMessageListener a) {
RedisMessageListenerContainer container
= new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
List<Topic> topics = Lists.newArrayList(new ChannelTopic(
CHANNEL),
new ChannelTopic(CHANNEL)
);
container.addMessageListener(new MessageListenerAdapter(a), topics);
return container;
}

启动分析

添加频道监听

//RedisMessageListenerContainer.java

public void addMessageListener(MessageListener listener, Collection<? extends Topic> topics) {
addListener(listener, topics);
lazyListen();
}

这个AddListener会 对Topic做一些记录,patternMapping, channelMapping,去重等等,然后最关键的一步:

//RedisMessageListenerContainer.java
//addListener
// check the current listening state
if (listening) {
subscriptionTask.subscribeChannel(channels.toArray(new byte[channels.size()][]));
subscriptionTask.subscribePattern(patterns.toArray(new byte[patterns.size()][]));
}
//RedisMessageListenerContainer.java

void subscribeChannel(byte[]... channels) {
if (channels != null && channels.length > 0) {
if (connection != null) {
synchronized (localMonitor) {
Subscription sub = connection.getSubscription();
if (sub != null) {
sub.subscribe(channels);
}
}
}
}
}
//JedisSubscription.java
protected void doSubscribe(byte[]... channels) {
jedisPubSub.subscribe(channels);
}

但是启动之前 这个listening=false。故该代码不生效。再看lazyListen方法:

//RedisMessageListenerContainer.java
private void lazyListen() {
boolean debug = logger.isDebugEnabled();
boolean started = false; if (isRunning()) {
if (!listening) {
synchronized (monitor) {
if (!listening) {
if (channelMapping.size() > 0 || patternMapping.size() > 0) {
subscriptionExecutor.execute(subscriptionTask);
listening = true;
started = true;
}
}
}
if (debug) {
if (started) {
logger.debug("Started listening for Redis messages");
} else {
logger.debug("Postpone listening for Redis messages until actual listeners are added");
}
}
}
}
}

调用addMessageListener的时候,isRunning()=false 也不生效。

最后:当@Bean构造完成的时候 ,LifeCycle进入start的时候,该Container启动。

//RedisMessageListenerContainer.java

    public void start() {
if (!running) {
running = true;
// wait for the subscription to start before returning
// technically speaking we can only be notified right before the subscription starts
synchronized (monitor) {
lazyListen();
if (listening) {
try {
// wait up to 5 seconds for Subscription thread
monitor.wait(initWait);
} catch (InterruptedException e) {
// stop waiting
}
}
} if (logger.isDebugEnabled()) {
logger.debug("Started RedisMessageListenerContainer");
}
}
}

这个时候,running=true了。

然后调用 lazyListen(),确实比较Lazy。

这个时候,启动子线程来执行订阅和监听。

subscriptionExecutor.execute(subscriptionTask);

这个subscriptionTask的构造如下:

//RedisMessageListenerContainer.java
public void run() {
synchronized (localMonitor) {
subscriptionTaskRunning = true;
}
try {
connection = connectionFactory.getConnection();
if (connection.isSubscribed()) {
throw new IllegalStateException("Retrieved connection is already subscribed; aborting listening");
} boolean asyncConnection = ConnectionUtils.isAsync(connectionFactory); // NB: async drivers' Xsubscribe calls block, so we notify the RDMLC before performing the actual subscription.
if (!asyncConnection) {
synchronized (monitor) {
monitor.notify();
}
} SubscriptionPresentCondition subscriptionPresent = eventuallyPerformSubscription(); if (asyncConnection) {
SpinBarrier.waitFor(subscriptionPresent, getMaxSubscriptionRegistrationWaitingTime()); synchronized (monitor) {
monitor.notify();
}
}
} catch (Throwable t) {
handleSubscriptionException(t);
} finally {
// this block is executed once the subscription thread has ended, this may or may not mean
// the connection has been unsubscribed, depending on driver
synchronized (localMonitor) {
subscriptionTaskRunning = false;
localMonitor.notify();
}
}
}

这里connection 肯定不是subscribed。

然后他根据Redis的客户端类型来判断是否是阻塞的

如果是阻塞的类型,则唤醒一下被阻塞的Container线程。(???)

然后,最关键的是:eventuallyPerformSubscription(),最终发起订阅的,并轮询订阅的是方法。

//RDMLC

private SubscriptionPresentCondition eventuallyPerformSubscription() {

	SubscriptionPresentCondition condition = null;

	if (channelMapping.isEmpty()) {

		condition = new PatternSubscriptionPresentCondition();
connection.pSubscribe(new DispatchMessageListener(), unwrap(patternMapping.keySet()));
} else { if (patternMapping.isEmpty()) {
condition = new SubscriptionPresentCondition();
} else {
// schedule the rest of the subscription
subscriptionExecutor.execute(new PatternSubscriptionTask());
condition = new PatternSubscriptionPresentCondition();
} connection.subscribe(new DispatchMessageListener(), unwrap(channelMapping.keySet()));
} return condition;
}

以connection.subscribe()为例:即将发起订阅,注意这里是利用DispatchMessageListener做的事件分发监听器。

//JedisConnection.java

public void subscribe(MessageListener listener, byte[]... channels) {
if (isSubscribed()) {
throw new RedisSubscribedConnectionException(
"Connection already subscribed; use the connection Subscription to cancel or add new channels");
} if (isQueueing()) {
throw new UnsupportedOperationException();
}
if (isPipelined()) {
throw new UnsupportedOperationException();
} try {
BinaryJedisPubSub jedisPubSub = new JedisMessageListener(listener); subscription = new JedisSubscription(listener, jedisPubSub, channels, null);
jedis.subscribe(jedisPubSub, channels); } catch (Exception ex) {
throw convertJedisAccessException(ex);
}
}
//BinaryJedis.java

public void subscribe(BinaryJedisPubSub jedisPubSub, byte[]... channels) {
client.setTimeoutInfinite();
try {
jedisPubSub.proceed(client, channels);
} finally {
client.rollbackTimeout();
}
}

这里调用了BinaryJedisPubSub的proceed()。

这里先提出两个问题?

要订阅是不是要发起subscribe命令给Redis?发起 subscribe channel命令,然后Listener怎么办?

这里调用是jedis.subscribe(jedisPubSub, channels);而一开始 subscibeChannels的实现却不太一样?

下面看jedisPubSub:

  public void proceed(Client client, byte[]... channels) {
this.client = client;
client.subscribe(channels);
client.flush();
process(client);
}

这里subscribe是再次发起订阅请求,然后process轮询检查消息。

异常处理

再看看JedisConnection类subscribe方法的异常的处理:

protected DataAccessException convertJedisAccessException(Exception ex) {

	if (ex instanceof NullPointerException) {
// An NPE before flush will leave data in the OutputStream of a pooled connection
broken = true;
} DataAccessException exception = EXCEPTION_TRANSLATION.translate(ex);
if (exception instanceof RedisConnectionFailureException) {
broken = true;
} return exception;
}

EXCEPTION_TRANSLATION.translate(ex); 会调用:PassThroughExceptionTranslationStrategy的Convert。

public class JedisExceptionConverter implements Converter<Exception, DataAccessException> {

	public DataAccessException convert(Exception ex) {

		if (ex instanceof DataAccessException) {
return (DataAccessException) ex;
}
if (ex instanceof JedisDataException) {
return new InvalidDataAccessApiUsageException(ex.getMessage(), ex);
}
if (ex instanceof JedisConnectionException) {
return new RedisConnectionFailureException(ex.getMessage(), ex);
}
if (ex instanceof JedisException) {
return new InvalidDataAccessApiUsageException(ex.getMessage(), ex);
}
if (ex instanceof UnknownHostException) {
return new RedisConnectionFailureException("Unknown host " + ex.getMessage(), ex);
}
if (ex instanceof IOException) {
return new RedisConnectionFailureException("Could not connect to Redis server", ex);
} return null;
}
}

那么,当Jedis抛错:JedisConnectionException 服务器似乎断开了连接

这个时候,subscribe 从而抛出RedisConnectionFailureException。

最后,再看RedisMessageListenerContainerd的run方法内的异常处理:

这个时候,

protected void handleSubscriptionException(Throwable ex) {
listening = false;
subscriptionTask.closeConnection();
if (ex instanceof RedisConnectionFailureException) {
if (isRunning()) {
logger.error("Connection failure occurred. Restarting subscription task after " + recoveryInterval + " ms");
sleepBeforeRecoveryAttempt();
lazyListen();
}
} else {
logger.error("SubscriptionTask aborted with exception:", ex);
}
}

到这个时候,isRunning还是true的(当且仅当LifeCycle进入close的时候,才会变成false),结果就会在 recoveryInterval ms之后,重启调用lazyListen(),再次启动订阅和监听。

实际效果

实际上,我在服务器上的错误日志中,我确实看到了

Connection failure occurred. Restarting subscription task after 5000 ms

总结

SpringData-Redis,能够解决手动处理Redis pub/sub的订阅被意外断开,导致监听失败的问题。

他能确保,服务持续监听,出现异常时,能够重新订阅并监听给定的频道。

所以,还是用框架吧,比自己手写的发布订阅更可靠。

SpringData-Redis发布订阅自动重连分析的更多相关文章

  1. 我在生产项目里是如何使用Redis发布订阅的?(一)使用场景

    转载请注明出处! 导语 Redis是我们很常用的一款nosql数据库产品,我们通常会用Redis来配合关系型数据库一起使用,弥补关系型数据库的不足. 其中,Redis的发布订阅功能也是它的一大亮点.虽 ...

  2. Redis 发布订阅,小功能大用处,真没那么废材!

    今天小黑哥来跟大家介绍一下 Redis 发布/订阅功能. 也许有的小伙伴对这个功能比较陌生,不太清楚这个功能是干什么的,没关系小黑哥先来举个例子. 假设我们有这么一个业务场景,在网站下单支付以后,需要 ...

  3. RedisRepository封装—Redis发布订阅以及StackExchange.Redis中的使用

    本文版权归博客园和作者本人吴双共同所有,转载请注明本Redis系列分享地址.http://www.cnblogs.com/tdws/tag/NoSql/ Redis Pub/Sub模式 基本介绍 Re ...

  4. python中使用redis发布订阅者模型

    redis发布订阅者模型: Redis提供了发布订阅功能,可以用于消息的传输,Redis的发布订阅机制包括三个部分,发布者,订阅者和Channel.发布者和订阅者都是Redis客户端,Channel则 ...

  5. Redis发布订阅机制

    1. 什么是Redis Redis是一个开源的内存数据库,它以键值对的形式存储数据.由于数据存储在内存中,因此Redis的速度很快,但是每次重启Redis服务时,其中的数据也会丢失,因此,Redis也 ...

  6. Linux(6)- redis发布订阅/持久化/主从复制/redis-sentinel/redis-cluster、nginx入门

    一.redis发布订阅 Redis 通过 PUBLISH .SUBSCRIBE 等命令实现了订阅与发布模式. 其实从Pub/Sub的机制来看,它更像是一个广播系统,多个Subscriber可以订阅多个 ...

  7. redis发布订阅客户端报错

    转自简书[https://www.jianshu.com/p/a85ec38245da] 最近遇到一个问题,springBoot程序中有一个监听器,监听redis中发来的消息(其实是监听一个key的消 ...

  8. SpringBoot Redis 发布订阅模式 Pub/Sub

    SpringBoot Redis 发布订阅模式 Pub/Sub 注意:redis的发布订阅模式不可以将消息进行持久化,订阅者发生网络断开.宕机等可能导致错过消息. Redis命令行下使用发布订阅 pu ...

  9. 把酒言欢话聊天,基于Vue3.0+Tornado6.1+Redis发布订阅(pubsub)模式打造异步非阻塞(aioredis)实时(websocket)通信聊天系统

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_202 "表达欲"是人类成长史上的强大"源动力",恩格斯早就直截了当地指出,处在蒙昧时代即低 ...

随机推荐

  1. redis 获取自增数

    近期,有一个项目需要用到数字的自增整数,范围是0-199999,但公司数据库是mongodb.同时装有mysql.redis等存储数据的这些数据库,其中redis是集群模式,mongodb是paa(  ...

  2. JAVASE知识点总结(三)

    第十六章:抽象类和接口 一.抽象方法:在方法面前加了abstract(为了解决,子类必须要覆盖此方法,在定义的时候不要方法体). 特点:1.抽象方法没有方法体. 2.抽象方法必须放在抽象类(类前面加上 ...

  3. linux centos安装zabbix 4.0服务端

    1.服务器安装docker sudo yum install -y yum-utils device-mapper-persistent-data lvm2 sudo yum-config-manag ...

  4. Vue:获取当前定位城市名

    实现思想:通过定位获取到当前所在城市名: 1.在工程目录index.html中引入: <script type="text/javascript" src="htt ...

  5. 让视频丝滑流畅——N/A通用补帧傻瓜解决方案

    补帧就是字面意思,把24帧的视频通过算法即时补偿到更高的帧数,获得更优秀的观感体验 索尼大法brivia电视的中高端产品线中的motionflow技术,都可以实现硬件补帧,只需要把动态打开,相应的画面 ...

  6. 彻底理解CORS跨域原理

    背景 现在的前端开发中都是前后端分离的开发模式,数据的获取并非同源,所以跨域的问题在我们日常开发中特别常见.其实这种资料网上也是一搜一大堆,但是都不够全面,理解起来也不够透彻.这篇文章就结合具体的示例 ...

  7. 设计模式----创建型模式之工厂模式(FactoryPattern)

    工厂模式主要分为三种简单工厂模式.工厂方法模式.抽象工厂模式三种.顾名思义,工厂主要是生产产品,作为顾客或者商家,我们不考虑工厂内部是怎么一个流程,我们要的是最终产品.将该种思路放在我们面向对象开发实 ...

  8. JavaScript设计模式——原型模式

    原型模式: 原型模式是指原型实例指向创建对象的种类,并通过拷贝这些原型创建新的对象,是一种用来创建对象的模式,也就是创建一个对象作为另一个对象的prototype属性: prototype警告:学习了 ...

  9. CSS实现带箭头的提示框

    我们在很多UI框架中看到带箭头的提示框,感觉挺漂亮,但是之前一直不知道其原理,今天网上找了些资料算是弄清楚原理了: 先上效果图: 原理分析: 上面的箭头有没有觉得很像一个三角形,是的,它就是三角形:只 ...

  10. 从零开始入门 K8s | Kubernetes 网络概念及策略控制

    作者 | 阿里巴巴高级技术专家  叶磊 一.Kubernetes 基本网络模型 本文来介绍一下 Kubernetes 对网络模型的一些想法.大家知道 Kubernetes 对于网络具体实现方案,没有什 ...