介绍及目标
发布/订阅在我们的系统开发中非常常用,redis也提供了这项功能,但实现的是比较基本的功能,对于可靠消息的发布订阅及消息堆积这些功能Redis是无法做到的。但我们依旧可以用来处理很多业务场景,比如:
我们系统夸服务去调用某些请求,但被调用的服务又没办法及时响应,而需要异步返回结果的时候,我们使用同步阻塞的话比较浪费资源拉低系统的吞吐能力,对于一些耗时的操作,因为长时间的等待还容易导致请求超时处理失败(实际可能已经处理成功了)。
对于上述的场景,我们可以考虑用redis的发布订阅功能去处理。
思路
基本思路是:
服务A先动态订阅主题1,随后调用服务B并把主题1的地址带过去,随后等待主题1返回消息。服务A订阅的主题设置超时机制,避免被调用服务长期不响应导致业务卡死。
服务B收到请求之后异步处理消息,在处理完业务之后通过主题1通知到服务A。
服务A收到服务B发送的消息之后在唤起等待的线程处理接下来的业务即可。这即有效的实现了应用的解耦,也提高了我们系统的吞吐能力。流程图如下:
核心代码
RedisChannelListenerConfig
这个是向spring容器注入redis发布订阅需要的bean
public class RedisChannelListenerConfig {
@Bean
public RedisMessageListenerContainer redisContainer(RedisConnectionFactory factory) {
final RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
return container;
}
}
RedisChannelService
封装发布订阅的方法
@Import(RedisChannelListenerConfig.class)
public class RedisChannelService {
/**
* 分割符
*/
private static final String PARTITION = "_";
@Resource
private RedisMessageListenerContainer container;
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 发布消息
* @param channel
* @param message
*/
public void publish(String channel, Object message) {
try {
redisTemplate.convertAndSend(channel, message);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 添加订阅渠道(单渠道)
*
* @param listener 监听器
* @param channel
*/
public void addSubscribe(MessageListener listener, String channel) {
container.addMessageListener(listener, new ChannelTopic(channel));
}
/**
* 模式订阅(支持通配符)
*
* @param listener
* @param channel
*/
public void addPSubscribe(MessageListener listener, String channel) {
container.addMessageListener(listener, new PatternTopic(channel));
}
/**
* 模式订阅(支持通配符)
*
* @param listener
* @param channels 多个通配主题
*/
public void addPSubscribe(MessageListener listener, List<String> channels) {
List<PatternTopic> list = new ArrayList<>();
for (String channel : channels) {
list.add(new PatternTopic(channel));
}
container.addMessageListener(listener, list);
}
/**
* 移除订阅
*
* @param listener
*/
public void removeSubscribe(MessageListener listener) {
container.removeMessageListener(listener);
}
/**
* 格式化消息
* @param isSuccess
* @param message
* @return
*/
public String formatMessage(Boolean isSuccess, String message){
return new StringBuilder().append(isSuccess.toString()).append(PARTITION).append(message).toString();
}
/**
* 获取消息值
* @param message
* @return
*/
public RedisChannelMessage getMessage(String message){
if(message == null || message.indexOf(PARTITION) == -1){
return null;
}
String[] split = message.split(PARTITION);
return new RedisChannelMessage(Boolean.valueOf(split[0]), split[1]);
}
}
RedisChannelMessage
@Data
@AllArgsConstructor
public class RedisChannelMessage {
private boolean isSuccess;
private String message;
}
RedisChannelListener
自定义监听器,设置topic并获取返回值
/**
* channel订阅模式通用类
*
* @author: yan
* @date: 2023/04/27
*/
public class RedisChannelListener<T> implements Consumer<T> {
private String channel;
private volatile T value;
private Thread thread;
public RedisChannelListener(String channel, Thread thread) {
this.channel = channel;
this.thread = thread;
}
@Override
public void accept(T t) {
this.value = t;
}
public String getChannel() {
return channel;
}
public T getValue() throws TimeoutException {
return getValue(10, TimeUnit.SECONDS);
}
public Thread getThread() {
return this.thread;
}
public T getValue(long time, TimeUnit timeUnit) throws TimeoutException {
long end = System.nanoTime() + timeUnit.toNanos(time);
while (System.nanoTime() < end) {
if (value != null) {
return value;
}
LockSupport.parkNanos(1000 * 1000 * 1000);
}
throw new TimeoutException();
}
}
以上就是对发布订阅模式的封装了
使用
以上配置默认不会注入容器,可根据项目需要初始化
RedisChannelConfig
初始化类
@Slf4j
@Component
@Import(RedisChannelService.class)
public class RedisChannelConfig {
@Resource
private RedisChannelService redisChannelService;
private static Cache<String, RedisChannelListener<String>> listenerMap = CacheBuilder.newBuilder()
// 300秒自动过期
.expireAfterWrite(300, TimeUnit.SECONDS)
.build();
@PostConstruct
public void init(){
// 开启全局订阅
final MessageListener messageListener = (message, bytes) -> {
String channel = new String(message.getChannel());
System.out.println(channel);
RedisChannelListener<String> listener = listenerMap.getIfPresent(channel);
if (listener != null) {
listener.accept(message.toString());
listenerMap.invalidate(channel);
}
};
// 这里限定统配订阅
ArrayList<String> channels = Lists.newArrayList("test_*");
redisChannelService.addPSubscribe(messageListener, channels);
}
public void registerListener(RedisChannelListener listener) {
listenerMap.put(listener.getChannel(), listener);
}
public void removeListener(RedisChannelListener listener) {
listenerMap.invalidate(listener.getChannel());
}
}
RedisChannelTest
使用方式,模拟调用远程服务
/**
* @author yan
* @date 2023-10-23
*/
@Service
public class RedisChannelTest {
@Resource
private RedisChannelConfig redisChannelConfig;
@Resource
private RedisChannelRemote redisChannelRemote;
public boolean sendTest() throws TimeoutException {
// 先订阅test_001消息
String channel = "test_001";
RedisChannelListener<String> listener = new RedisChannelListener<>(channel, Thread.currentThread());
redisChannelConfig.registerListener(listener);
// 调用外部服务
String handle = redisChannelRemote.handle("test,test", channel);
System.out.println(handle);
// 获取结果
String value = listener.getValue();
System.out.println(value);
return true;
}
}
RedisChannelRemote
远程服务
/**
* @author yan
* @date 2023-10-23
*/
@Service
public class RedisChannelRemote {
@Resource
private RedisChannelService redisChannelService;
public String handle(String msg, String topic){
new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
redisChannelService.publish(topic, "back:" + msg);
}).start();
return "success";
}
}
RedisController
最后写个controller试一下
@RestController
@RequestMapping(value = "redis")
public class RedisController {
@Resource
private RedisChannelTest redisChannelTest;
@GetMapping("channel")
public String channel() throws TimeoutException {
if (redisChannelTest.sendTest()) {
return "success";
}
return "fail";
}
}
总结
以上就是使用Redis发布订阅功能实现动态注册监听及异步获取结果。