Netty实战应用学习笔记-中级拓展篇(九)

🌐 项目九:集群部署实现跨服务端通信

项目概述

项目名称: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集成:简化配置和管理

架构优势

  1. 解耦服务器:服务器之间不需要直接连接
  2. 动态扩展:新增服务器无需修改现有服务器
  3. 连接数线性增长:N台服务器只需N个Redis连接
  4. 统一管理:通过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-07Future + CountDownLatch异步转同步RPC框架、同步调用
2-08IdleStateHandler + 重连连接保活长连接系统、IM
2-09Redis Pub/Sub + 集群跨服务器通信分布式IM、游戏

核心知识点总结

1. 同步通信(项目2-07)

核心原理

发送请求 → 创建Future → 阻塞等待 → 收到响应 → 唤醒线程

关键技术

  • CountDownLatch:线程阻塞和唤醒
  • 请求ID映射:准确匹配请求和响应
  • 超时控制:避免无限等待

类比理解

  • 像打电话:拨号后等待对方接听,接听后才能继续
2. 心跳重连(项目2-08)

核心原理

空闲检测 → 触发事件 → 发送心跳/关闭连接 → 自动重连

关键技术

  • IdleStateHandler:自动检测空闲状态
  • ChannelFutureListener:监听连接结果
  • EventLoop.schedule:延迟重连

类比理解

  • 像微信聊天:定期检测网络,断线后自动重连
3. 集群通信(项目2-09)

核心原理

本地查找 → 未找到 → Redis发布 → 其他服务器订阅 → 转发消息

关键技术

  • Redis Pub/Sub:消息中转站
  • 两级缓存:本地+Redis
  • 消息路由:智能转发

类比理解

  • 像快递系统:不是站点直连,而是通过分拨中心转发

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值