🌐 项目九:集群部署实现跨服务端通信
项目概述
项目名称:itstack-demo-netty-2-09
核心功能:使用Redis Pub/Sub实现Netty集群间的跨服务器通信
技术要点:Redis发布订阅、两级缓存、消息路由、Spring Boot集成
为什么需要这个项目?
实际问题:
- 单台服务器无法支撑大量用户连接
- 集群部署后,用户A和用户B可能连接到不同服务器
- 不同服务器的用户之间无法直接通信
- Netty服务器之间直连,连接数指数增长
解决方案:
- ✅ Redis Pub/Sub:作为消息中转站,解耦服务器
- ✅ 两级缓存:本地内存+Redis,提高查询效率
- ✅ 消息路由:先查本地,再查全局
- ✅ 动态扩展:新增服务器无需修改现有服务器
适用场景:
- IM即时通讯系统(微信、QQ的集群架构)
- 游戏服务器(跨服务器组队、交易)
- 物联网平台(设备跨网关通信)
- 分布式推送系统
核心问题
集群部署的通信难题
当Netty以集群方式部署时:
- 用户A连接到服务器X
- 用户B连接到服务器Y
- A和B不在同一个服务器,如何实现通信?
两种解决方案对比:
| 方案 | Netty服务器直连 | Redis消息中间件 |
|---|---|---|
| 连接数 | N×(N-1) 指数增长 | N 线性增长 |
| 服务发现 | 需要额外的注册中心 | Redis本身就是注册中心 |
| 消息路由 | 需要自己实现 | Redis Pub/Sub自动分发 |
| 扩展性 | 新增服务器需修改所有服务器 | 新增服务器无需修改其他服务器 |
| 复杂度 | 高 | 低 |
为什么选择Redis?
Netty直连方案:
服务器A ←→ 服务器B
↕ ↕
服务器C ←→ 服务器D
(需要维护6个连接)
Redis方案:
服务器A → Redis ← 服务器B
服务器C → Redis ← 服务器D
(只需要4个连接)
核心知识点
1. 系统架构设计
┌─────────────┐ ┌─────────────┐
│ 用户A │ │ 用户B │
└──────┬──────┘ └──────┬──────┘
│ │
│ Netty连接 │ Netty连接
↓ ↓
┌─────────────┐ ┌─────────────┐
│ Netty服务器X │ │ Netty服务器Y │
└──────┬──────┘ └──────┬──────┘
│ │
│ 发布消息 │ 订阅消息
↓ ↓
└──────────→ Redis ←───────────────┘
Pub/Sub
通信流程:
1. 用户A发送消息给用户B
2. 服务器X接收消息,发现用户B不在本服务器
3. 服务器X通过Redis发布消息
4. Redis广播消息到所有订阅者(服务器X、Y、Z...)
5. 服务器Y接收到订阅消息
6. 服务器Y从本地缓存查找用户B的Channel
7. 服务器Y将消息发送给用户B
2. 核心领域对象
MsgAgreement.java - 消息协议
public class MsgAgreement {
private String toChannelId; // 接收者的channelId
private String content; // 消息内容
}
UserChannelInfo.java - 用户连接信息
public class UserChannelInfo {
private String ip; // 服务器IP
private int port; // 服务器端口
private String channelId; // 用户channelId
private Date linkDate; // 连接时间
}
3. 两级缓存机制
为什么需要两级缓存?
本地内存缓存(CacheUtil):
- 快速判断用户是否在本服务器
- 避免每次都查Redis
- 存储本服务器的Channel对象
Redis缓存(RedisUtil):
- 全局共享用户分布信息
- 方便管理和监控
- 存储所有服务器的用户信息
CacheUtil.java - 本地缓存
public class CacheUtil {
// 缓存本服务器的Channel
public static Map<String, Channel> cacheChannel =
Collections.synchronizedMap(new HashMap<>());
// 缓存服务器信息
public static Map<Integer, ServerInfo> serverInfoMap =
Collections.synchronizedMap(new HashMap<>());
// 缓存Netty服务器实例
public static Map<Integer, NettyServer> serverMap =
Collections.synchronizedMap(new HashMap<>());
}
RedisUtil.java - Redis缓存
@Service("redisUtil")
public class RedisUtil {
@Autowired
private StringRedisTemplate redisTemplate;
// 保存用户信息到Redis
public void pushObj(UserChannelInfo userChannelInfo) {
redisTemplate.opsForHash().put(
"itstack-demo-netty-2-09-user",
userChannelInfo.getChannelId(),
JSON.toJSONString(userChannelInfo)
);
}
// 查询所有用户信息
public List<UserChannelInfo> popList() {
List<Object> values = redisTemplate.opsForHash()
.values("itstack-demo-netty-2-09-user");
List<UserChannelInfo> userChannelInfoList = new ArrayList<>();
for (Object strJson : values) {
userChannelInfoList.add(
JSON.parseObject(strJson.toString(), UserChannelInfo.class)
);
}
return userChannelInfoList;
}
// 移除用户信息
public void remove(String channelId) {
redisTemplate.opsForHash().delete(
"itstack-demo-netty-2-09-user", channelId
);
}
}
4. Redis发布订阅机制
发布者配置(PublisherConfig.java)
@Configuration
public class PublisherConfig {
@Bean
public RedisTemplate<String, Object> redisMessageTemplate(
RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setDefaultSerializer(
new FastJsonRedisSerializer<>(Object.class)
);
return template;
}
}
订阅者配置(ReceiverConfig.java)
@Configuration
public class ReceiverConfig {
@Bean
public RedisMessageListenerContainer container(
RedisConnectionFactory connectionFactory,
MessageListenerAdapter msgAgreementListenerAdapter) {
RedisMessageListenerContainer container =
new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 订阅主题:itstack-demo-netty-push-msgAgreement
container.addMessageListener(
msgAgreementListenerAdapter,
new PatternTopic("itstack-demo-netty-push-msgAgreement")
);
return container;
}
@Bean
public MessageListenerAdapter msgAgreementListenerAdapter(
MsgAgreementReceiver receiver) {
return new MessageListenerAdapter(receiver, "receiveMessage");
}
}
发布消息(Publisher.java)
@Service
public class Publisher {
private final RedisTemplate<String, Object> redisMessageTemplate;
public void pushMessage(String topic, MsgAgreement message) {
// 向指定主题发布消息
redisMessageTemplate.convertAndSend(topic, message);
}
}
接收消息(MsgAgreementReceiver.java)
@Service
public class MsgAgreementReceiver extends AbstractReceiver {
@Override
public void receiveMessage(Object message) {
logger.info("接收到PUSH消息:{}", message);
// 解析消息
MsgAgreement msgAgreement = JSON.parseObject(
message.toString(), MsgAgreement.class
);
String toChannelId = msgAgreement.getToChannelId();
// 从本地缓存查找目标用户的Channel
Channel channel = CacheUtil.cacheChannel.get(toChannelId);
if (null == channel) return;
// 发送消息给目标用户
channel.writeAndFlush(MsgUtil.obj2Json(msgAgreement));
}
}
5. 消息路由逻辑
MyServerHandler.java
@Override
public void channelActive(ChannelHandlerContext ctx) {
SocketChannel channel = (SocketChannel) ctx.channel();
// 1. 保存用户信息到Redis(全局共享)
UserChannelInfo userChannelInfo = new UserChannelInfo(
channel.localAddress().getHostString(),
channel.localAddress().getPort(),
channel.id().toString(),
new Date()
);
extServerService.getRedisUtil().pushObj(userChannelInfo);
// 2. 保存到本地缓存(快速查找)
CacheUtil.cacheChannel.put(channel.id().toString(), channel);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object objMsgJsonStr) {
MsgAgreement msgAgreement = MsgUtil.json2Obj(objMsgJsonStr.toString());
String toChannelId = msgAgreement.getToChannelId();
// 1. 先查找本地缓存,判断接收者是否在本服务器
Channel channel = CacheUtil.cacheChannel.get(toChannelId);
if (null != channel) {
// 接收者在本服务器,直接发送
channel.writeAndFlush(MsgUtil.obj2Json(msgAgreement));
return;
}
// 2. 接收者不在本服务器,通过Redis发布消息
logger.info("接收消息的用户不在本服务端,PUSH!");
extServerService.push(msgAgreement);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
// 连接断开,清理缓存
extServerService.getRedisUtil().remove(ctx.channel().id().toString());
CacheUtil.cacheChannel.remove(ctx.channel().id().toString());
}
ExtServerService.java - 扩展服务
@Service("extServerService")
public class ExtServerService {
@Resource
private Publisher publisher;
@Resource
private RedisUtil redisUtil;
// 发布消息到Redis
public void push(MsgAgreement msgAgreement) {
publisher.pushMessage(
"itstack-demo-netty-push-msgAgreement",
msgAgreement
);
}
public RedisUtil getRedisUtil() {
return redisUtil;
}
}
6. Spring Boot集成
NettyController.java - HTTP接口管理
@Controller
public class NettyController {
@Autowired
private ExtServerService extServerService;
@Resource
private RedisUtil redisUtil;
// 启动Netty服务
@RequestMapping("/openNettyServer")
@ResponseBody
public EasyResult openNettyServer() {
int port = NetUtil.getPort(); // 动态获取可用端口
nettyServer = new NettyServer(
new InetSocketAddress(port), extServerService
);
Future<Channel> future = executorService.submit(nettyServer);
Channel channel = future.get();
// 缓存服务器信息
CacheUtil.serverInfoMap.put(port,
new ServerInfo(NetUtil.getHost(), port, new Date())
);
CacheUtil.serverMap.put(port, nettyServer);
return EasyResult.buildSuccessResult();
}
// 查询用户列表
@RequestMapping("/queryUserChannelInfoList")
@ResponseBody
public List<UserChannelInfo> queryUserChannelInfoList() {
return redisUtil.popList();
}
}
项目总结
核心技术:
- ✅ Redis Pub/Sub:实现跨服务器消息传递
- ✅ 两级缓存:本地内存+Redis,提高查询效率
- ✅ 消息路由:先查本地,再查全局
- ✅ Spring Boot集成:简化配置和管理
架构优势:
- 解耦服务器:服务器之间不需要直接连接
- 动态扩展:新增服务器无需修改现有服务器
- 连接数线性增长:N台服务器只需N个Redis连接
- 统一管理:通过Redis集中管理用户分布
适用场景:
- IM即时通讯系统(微信、QQ)
- 游戏服务器(跨服务器组队、交易)
- 物联网平台(设备跨网关通信)
- 分布式推送系统
注意事项:
- ⚠️ Redis单点故障问题(建议使用Redis Cluster)
- ⚠️ 消息可靠性(当前实现没有ACK确认)
- ⚠️ 离线消息处理(需要额外存储)
优化建议:
// 1. 定向发送(避免广播)
String targetServer = redisUtil.getUserServer(toChannelId);
publisher.pushMessage("server-" + targetServer, msgAgreement);
// 2. 消息确认机制
public void sendWithAck(MsgAgreement msg) {
String ackId = UUID.randomUUID().toString();
msg.setAckId(ackId);
publisher.pushMessage(topic, msg);
// 等待ACK或超时重发
}
// 3. 离线消息存储
if (channel == null) {
redisUtil.saveOfflineMsg(toChannelId, msgAgreement);
}
🎯 三大项目核心对比
技术选型对比
| 项目 | 核心技术 | 解决问题 | 应用场景 |
|---|---|---|---|
| 2-07 | Future + CountDownLatch | 异步转同步 | RPC框架、同步调用 |
| 2-08 | IdleStateHandler + 重连 | 连接保活 | 长连接系统、IM |
| 2-09 | Redis Pub/Sub + 集群 | 跨服务器通信 | 分布式IM、游戏 |
核心知识点总结
1. 同步通信(项目2-07)
核心原理:
发送请求 → 创建Future → 阻塞等待 → 收到响应 → 唤醒线程
关键技术:
- CountDownLatch:线程阻塞和唤醒
- 请求ID映射:准确匹配请求和响应
- 超时控制:避免无限等待
类比理解:
- 像打电话:拨号后等待对方接听,接听后才能继续
2. 心跳重连(项目2-08)
核心原理:
空闲检测 → 触发事件 → 发送心跳/关闭连接 → 自动重连
关键技术:
- IdleStateHandler:自动检测空闲状态
- ChannelFutureListener:监听连接结果
- EventLoop.schedule:延迟重连
类比理解:
- 像微信聊天:定期检测网络,断线后自动重连
3. 集群通信(项目2-09)
核心原理:
本地查找 → 未找到 → Redis发布 → 其他服务器订阅 → 转发消息
关键技术:
- Redis Pub/Sub:消息中转站
- 两级缓存:本地+Redis
- 消息路由:智能转发
类比理解:
- 像快递系统:不是站点直连,而是通过分拨中心转发
5191

被折叠的 条评论
为什么被折叠?



