redis發佈訂閱架構
Redis提供了發佈訂閱功能,可以用於消息的傳輸,Redis的發佈訂閱機制包括三個部分,發佈者,訂閱者和Channel。
發佈者和訂閱者都是Redis客戶端,Channel則爲Redis服務器端,發佈者將消息發送到某個的頻道,訂閱了這個頻道的訂閱者就能接收到這條消息。Redis的這種發佈訂閱機制與基於主題的發佈訂閱類似,Channel相當於主題。
JAVA實現方式
redis配置
@Configuration
public class RedisConfig {
@Value("${redis.pool.minIdle}")
private int minIdle;
@Value("${redis.pool.maxTotal}")
private int maxTotal;
@Value("${redis.pool.maxWaitMills}")
private int maxWaitMills;
@Value("${redis.pool.testWhileIdle}")
private boolean testWhileIdle;
@Value("${redis.pool.testOnBorrow}")
private boolean testOnBorrow;
@Value("${redis.pool.testOnReturn}")
private boolean testOnReturn;
@Value("${redis.pool.maxIdle}")
private int maxIdle;
@Value("${redis.pool.blockWhenExhausted}")
private boolean blockWhenExhausted;
@Value("${redis.pool.minEvictableIdleTimeMillis}")
private int minEvictableIdleTimeMillis;
@Value("${redis.pool.timeBetweenEvictionRunsMillis}")
private int timeBetweenEvictionRunsMillis;
@Value("${redis.pool.numTestsPerEvictionRun}")
private int numTestsPerEvictionRun;
@Value("${redis.cluster.host}")
private String host;
@Value("${redis.cluster.port}")
private int port;
@Value("${redis.cluster.Password}")
private String redisPassword;
@Bean
public JedisPoolConfig jedisPoolConfig() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(maxTotal);
poolConfig.setMaxIdle(maxIdle);
poolConfig.setMinIdle(minIdle);
poolConfig.setMaxWaitMillis(maxWaitMills);
poolConfig.setTestOnBorrow(testOnBorrow);
poolConfig.setTestOnReturn(testOnReturn);
poolConfig.setTestWhileIdle(testWhileIdle);
poolConfig.setBlockWhenExhausted(blockWhenExhausted);
poolConfig.setNumTestsPerEvictionRun(numTestsPerEvictionRun);
poolConfig.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
poolConfig.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
return poolConfig;
}
@Bean
public JedisPool jedisPool(){
return new JedisPool(jedisPoolConfig(), host, port, Protocol.DEFAULT_TIMEOUT, redisPassword);
}
}
發佈者
public class Publisher extends Thread {
private String channel;
private JedisPool jedisPool;
public Publisher(String channel, JedisPool jedisPool) {
this.channel = channel;
this.jedisPool = jedisPool;
}
@Override
public void run() {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
Jedis jedis = jedisPool.getResource();
while (true) {
try {
String line = reader.readLine();
if (StringUtils.isNotEmpty(line)) {
if ("quit".equals(line)) {
System.exit(0);
}
jedis.publish(channel, line);
} else {
System.out.println("請輸入消息");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
訂閱者
public class Subscriber extends Thread {
private String channel;
private JedisPool jedisPool;
private MessageListener messageListener;
public Subscriber(String channel, JedisPool jedisPool, MessageListener messageListener) {
this.channel = channel;
this.jedisPool = jedisPool;
this.messageListener = messageListener;
}
@Override
public void run() {
System.out.println(String.format("subscribe redis, channel %s, thread will be blocked", Constant.CHANNEL));
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.subscribe(messageListener, channel);
} catch (Exception e) {
System.out.println(String.format("subsrcibe channel error, %s", e));
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
@Component
public class MessageListener extends JedisPubSub {
/**
* 功能描述: 收到消息會調用<br>
*
* @Param: [channel, message]
* @Return: void
* @Author: jiangtj
* @Date: 2020/4/22 15:01
*/
@Override
public void onMessage(String channel, String message) {
System.out.println(String.format("receive redis published message, channel %s, message %s", channel, message));
}
/**
* 功能描述: 訂閱了頻道會調用<br>
*
* @Param: [channel, subscribedChannels]
* @Return: void
* @Author: jiangtj
* @Date: 2020/4/22 15:01
*/
@Override
public void onSubscribe(String channel, int subscribedChannels) {
System.out.println(String.format("subscribe redis channel success, channel %s, subscribedChannels %d",
channel, subscribedChannels));
}
/**
* 功能描述: 取消訂閱會調用<br>
*
* @Param: [channel, subscribedChannels]
* @Return: void
* @Author: jiangtj
* @Date: 2020/4/22 15:01
*/
@Override
public void onUnsubscribe(String channel, int subscribedChannels) {
System.out.println(String.format("unsubscribe redis channel, channel %s, subscribedChannels %d",
channel, subscribedChannels));
}
}
使用CommandLineRunner在啓動時觸發事件
public class Constant {
public static final String CHANNEL = "TEST";
}
@Component
public class StartRunner implements CommandLineRunner {
@Autowired
private JedisPool jedisPool;
@Autowired
private MessageListener messageListener;
@Override
public void run(String... args) throws Exception {
Subscriber subscriber = new Subscriber(Constant.CHANNEL, jedisPool, messageListener);
subscriber.start();
Publisher publisher = new Publisher(Constant.CHANNEL, jedisPool);
publisher.start();
}
}
啓動類
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
public class RedisApplication {
public static void main(String[] args) {
try {
SpringApplication.run(RedisApplication.class, args);
} catch (Exception e) {
e.printStackTrace();
}
}
}
總結:
雖然redis實現了發佈訂閱(publish/subscribe)的功能,但是在通常的情況下是不推薦使用的,如果想使用消息隊列這種功能,最好還是使用專業的各種MQ中間件,例如rabbitMQ,rockedMQ,activitedMQ等,下面主要講一下不推薦使用redis的發佈訂閱功能的原因。
概要說一下就是,PUBLISH和SUBSCRIBE的缺陷在於客戶端必須一直在線才能接收到消息,斷線可能會導致客戶端丟失消息,除此之外,舊版的redis可能會由於訂閱者消費不夠快而變的不穩定導致崩潰,甚至被管理員殺掉
第一個原因是和redis系統的穩定性有關。對於舊版的redis來說,如果一個客戶端訂閱了某個或者某些頻道,但是它讀取消息的速度不夠快,那麼不斷的積壓的消息就會使得redis輸出緩衝區的體積越來越大,這可能會導致redis的速度變慢,甚至直接崩潰。也可能會導致redis被操作系統強制殺死,甚至導致操作系統本身不可用。新版的redis不會出現這種問題,因爲它會自動斷開不符合client-output-buffer-limit pubsub配置選項要求的訂閱客戶端。
第二個原因是和數據傳輸的可靠性有關。任何網絡系統在執行操作時都可能會遇到斷網的情況。而斷線產生的連接錯誤通常會使得網絡連接兩端中的一端進行重新連接。如果客戶端在執行訂閱操作的過程中斷線,那麼客戶端將會丟失在斷線期間的消息,這在很多業務場景下是不可忍受的。