加群联系作者vx:xiaoda0423
仓库地址:https://webvueblog.github.io/JavaPlusDoc/
https://github.com/webVueBlog/fastapi_plus
https://webvueblog.github.io/JavaPlusDoc/
点击勘误issues,哪吒感谢大家的阅读
Lettuce 和 Jedis 都是 Java 里用来连接 Redis 的客户端库 ,作用是让你的 Java 程序能去读写 Redis 数据。
但它们的设计和使用方式不太一样,主要区别在:
1️⃣ 基本定位
特性 | Jedis | Lettuce |
---|---|---|
实现原理 | 基于直连 Socket | 基于 Netty(异步事件驱动) |
线程安全 | 非线程安全 (一个 Jedis 对象只能一个线程用) |
线程安全 (一个 Lettuce 连接可多线程共享) |
连接模式 | 每次需要时获取连接,用完关闭/归还到连接池 | 推荐长连接,可一个连接支持并发命令 |
支持模式 | 单机、主从、哨兵、集群 | 单机、主从、哨兵、集群,全支持 |
同步/异步 | 只支持同步 | 支持 同步 + 异步 + Reactive(响应式) |
2️⃣ 使用风格示例
Jedis 同步示例
go
import redis.clients.jedis.Jedis;
public class JedisDemo {
public static void main(String[] args) {
try (Jedis jedis = new Jedis("localhost", 6379)) {
jedis.set("name", "leo");
String value = jedis.get("name");
System.out.println(value);
}
}
}
缺点:
Jedis
对象不能多线程共享,要靠连接池管理。
Lettuce 同步/异步示例
go
import io.lettuce.core.RedisClient;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.api.async.RedisAsyncCommands;
import io.lettuce.core.api.StatefulRedisConnection;
public class LettuceDemo {
public static void main(String[] args) {
RedisClient client = RedisClient.create("redis://localhost:6379");
StatefulRedisConnection<String, String> connection = client.connect();
// 同步
RedisCommands<String, String> sync = connection.sync();
sync.set("name", "leo");
System.out.println(sync.get("name"));
// 异步
RedisAsyncCommands<String, String> async = connection.async();
async.get("name").thenAccept(System.out::println);
connection.close();
client.shutdown();
}
}
优点:一个连接就能支持多线程,且异步性能好,特别适合高并发和 Spring WebFlux 这种响应式架构。
3️⃣ 总结选型建议
-
Jedis:简单、老牌、容易上手,但线程安全差,需要连接池管理;适合老项目或低并发场景。
-
Lettuce:新项目首选,线程安全,支持异步/响应式,Spring Boot 2.x 之后默认使用 Lettuce 代替 Jedis。
如果你是在 Spring Boot + Redis ,其实 spring-boot-starter-data-redis
默认就是 Lettuce,不用自己额外引 Jedis,除非你特别想换。
一、最小可用(单机→双机)
1) 拿一台服务器先跑通(PoC)
-
一台机 :
Nginx (stream)
+Netty 网关
+Redis 单机
-
用途:先把"设备能连、心跳续期、映射写Redis、Nginx转发"跑起来
Nginx(TCP 转发)
go
# /etc/nginx/nginx.conf
stream {
upstream iot_gateway {
# 先单机,后面加节点
server 127.0.0.1:9000;
}
server {
listen 9000; # 设备连这个端口
proxy_connect_timeout 5s;
proxy_timeout 600s;
proxy_pass iot_gateway;
}
}
Netty(关键参数)
go
new ServerBootstrap()
.group(new NioEventLoopGroup(1), new NioEventLoopGroup()) // boss=1, worker=CPU核数
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 4096)
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new IoTChannelInitializer(connectionManager, gatewayIp));
Redis(键设计)
-
device:gateway:{deviceId} = {gatewayIp}
,TTL=120s -
每次收到心跳/数据 →
EXPIRE key 120
-
网关宕机=不再续期 → TTL 到 → 键过期 → 设备重连被分配到存活节点
2) 升级成两台网关 + Nginx
-
两台机 :
网关A
、网关B
-
一台机 :
Nginx
(对外),内部指向A/B -
Redis:先仍单机(下一步再高可用)
Nginx(加一致性哈希)
go
stream {
upstream iot_gateway {
hash $remote_addr consistent; # 按来源IP粘性(足够简单)
server 10.0.0.10:9000 max_fails=3 fail_timeout=30s; # 网关A
server 10.0.0.11:9000 max_fails=3 fail_timeout=30s; # 网关B
}
server {
listen 9000;
proxy_connect_timeout 5s;
proxy_timeout 600s;
proxy_pass iot_gateway;
}
}
说明:如果你的设备大量在同一运营商NAT 后面,
$remote_addr
可能"很多设备同一IP"。到百万级 时建议升级为"按设备ID粘性"(可用 Nginx njs preread 提前读首包提取ID做hash,或让前置接入层读首包后按设备ID转发------这是进阶方案,先不急)。
二、Redis 高可用(两种选一个)
方案A:Sentinel(主从 + 自动切主)
-
适合:容量要求一般、想简单点
-
Spring Boot 配置(节选):
go
spring:
redis:
sentinel:
master: mymaster
nodes: 10.0.0.21:26379,10.0.0.22:26379,10.0.0.23:26379
password: ${REDIS_AUTH:}
方案B:Cluster(分片 + 多副本)
- 适合:设备多、QPS高、容量大
go
spring:
redis:
cluster:
nodes: 10.0.0.31:6379,10.0.0.32:6379,10.0.0.33:6379
max-redirects: 3
password: ${REDIS_AUTH:}
建议:Cluster 更长远 。你只需要把应用的
application.yml
切到 cluster profile 即可。
三、网关健康上报 + 映射原子写 + 踢掉旧连接(必要的三件套)
1) 健康上报(每10s)
go
@Scheduled(fixedDelay = 10_000)
public void reportAlive() {
redis.opsForValue().set("gateway:alive:" + gatewayIp,
String.valueOf(System.currentTimeMillis()),
Duration.ofSeconds(30)); // TTL 30s
}
- 运维看
gateway:alive:*
就知道谁在线
2) 原子写映射(Lua)
-
目的:把
device:gateway:{id}
原子更新为当前网关IP,并返回旧IP(如果旧IP不同) -
用来跨网关"踢掉"旧连接,避免同一设备多地同时在线
Lua(简化版,够用):
go
-- KEYS[1]=deviceKey, ARGV[1]=newIp, ARGV[2]=ttlSec
local old=redis.call('GET', KEYS[1])
if(old==ARGV[1]) then
redis.call('EXPIRE', KEYS[1], tonumber(ARGV[2])); return {0}
end
redis.call('SET', KEYS[1], ARGV[1], 'EX', tonumber(ARGV[2]))
if(not old or old=='') then return {0} end
return {1, old} -- 需要踢掉 old 网关的同设备连接
Java 调用:设备首包时执行;若返回{1, oldIp}
→发消息给 oldIp
踢人(用 Redis Pub/Sub 或 Kafka)。
3) 踢掉旧连接(轻量版:Redis Pub/Sub)
-
发布:
{"type":"EVICT","target":"10.0.0.10","deviceId":"ABC123"}
-
订阅端在目标网关 拿到消息后
channel.close()
清理本机旧连接
四、操作系统 & JVM & Netty 调优(保姆版)
Linux 内核(/etc/sysctl.conf)
go
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 250000
net.ipv4.ip_local_port_range = 10000 65000
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_keepalive_time = 60
net.ipv4.tcp_keepalive_intvl = 10
net.ipv4.tcp_keepalive_probes = 3
应用:
go
sysctl -p
ulimit -n 1048576 # 打开文件句柄,决定最大连接数上限
Netty 线程
-
bossGroup = 1
-
workerGroup = CPU核数 ~ 2×CPU核数
(压测看情况) -
Epoll (Linux):使用
EpollEventLoopGroup
更省CPU
JVM
-
连接多时堆外内存 和DirectBuffer 占比高,Xms=Xmx 固定堆
-
GC:G1/ ZGC(二选一),尽量减少 stop-the-world
-
监控:Prometheus + Grafana(曲线看连接数、堆、GC、CPU)
五、容量估算 & 扩容阈值
1) 经验值(保守)
-
纯TCP长连接 + 轻协议:每连接 8--16KB RAM(含内核socket缓存 + 应用少量状态)
-
1 台 16GB 机器:80k--120k 长连接(还要留出 JVM/系统/监控余量)
-
CPU 不是瓶颈时,内存和FD是主要限制
2) 你可以这样定阈值(示例)
-
每台网关目标连接 :100k
-
触发扩容:任一网关 10 分钟均值 > 80k 或 JVM老年代>70% 或 CPU>60%
-
扩容操作:
-
新增一台网关(同样注册到 Nginx upstream)
-
Nginx reload(无损)
-
设备会在后续重连时被一致性哈希分摊到新节点
-
如果增长很快,一次性多上两台,少走回头路
六、你就照这个顺序做(最重要)
-
单机跑通(Nginx + 网关 + Redis 单机)
-
加一台网关 → Nginx upstream + consistent hash
-
把 Redis 换成 哨兵或集群
-
上 健康上报 、Lua 原子写 、跨网关踢人(三件套)
-
上 监控/告警(连接数、QPS、GC、负载)
-
设备量上来后:
-
网关横向扩容(加机器)
-
Redis 走 Cluster
-
Nginx 多实例 + 前面再加 L4 SLB/Anycast(多机房可选)
-
如果
$remote_addr
粘性不够,升级为按设备ID粘性(njs preread 或前置接入层)
七、你会遇到的坑(先知道=少走弯路)
-
NAT 同IP :很多设备同一个公网IP →
$remote_addr
粘性会让单个网关压力偏高。可先调大权重或加节点,后续再上按设备ID粘性。 -
TTL 误判:心跳周期×3 做TTL,避免偶发丢包导致过期误切换。
-
双连脏连接 :一定做Lua原子写 + 踢旧连接。
-
FD 不够 :
ulimit -n
不到位会很快耗尽。 -
监控缺失:没有连接数/GC/CPU/Redis延迟监控,很难定位抖动。
八、要配置清单(你可以拷走用)
-
Nginx(stream)模板:上面给了
-
Redis:Cluster 或 Sentinel 的
application.yml
模板:上面给了 -
网关:
ServerBootstrap
、ChannelInitializer
、ConnectionManager
(含续期)、Lua原子写
、Pub/Sub踢人
:上文都有 -
系统:
sysctl.conf
+ulimit
详单
想不想我直接给你一个docker-compose 一键起:
-
nginx
+redis-cluster
+gatewayA/B
+ 压测客户端 ;你本地跑起来立刻能看到:宕机迁移、TTL过期、踢旧连接 全流程。
一句话,你说"要",我就给你打包好。
我给你一个可复用的设备端 自动重连 TCP 客户端代码(Java 版),适合和你的 Spring Boot + Netty 网关配合使用,支持以下特性:
-
断线自动重连(指数退避重试)
-
心跳保活
-
消息收发
-
支持百万级连接测试时的稳定性
1. 设备端自动重连 TCP 客户端代码
go
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
import java.util.concurrent.TimeUnit;
public class DeviceClient {
private final String host;
private final int port;
private final String deviceId;
private EventLoopGroup group;
private Bootstrap bootstrap;
private volatile boolean reconnecting = false; // 避免重复重连
public DeviceClient(String host, int port, String deviceId) {
this.host = host;
this.port = port;
this.deviceId = deviceId;
init();
}
private void init() {
group = new NioEventLoopGroup(1); // 单线程模拟设备端
bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
// 60 秒没发数据触发 Idle 事件
p.addLast(new IdleStateHandler(60, 30, 0, TimeUnit.SECONDS));
p.addLast(new DeviceClientHandler(DeviceClient.this, deviceId));
}
});
}
public void connect() {
bootstrap.connect(host, port).addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
System.out.println(deviceId + " 连接成功: " + host + ":" + port);
reconnecting = false;
} else {
System.out.println(deviceId + " 连接失败,3秒后重试...");
scheduleReconnect(3);
}
});
}
public void scheduleReconnect(int delaySeconds) {
if (!reconnecting) {
reconnecting = true;
group.schedule(this::connect, delaySeconds, TimeUnit.SECONDS);
}
}
public void sendMessage(String msg) {
if (bootstrap != null && bootstrap.config().group() != null) {
bootstrap.config().group().execute(() -> {
try {
// 在 pipeline 中写数据
// 真实场景应保存 channel 引用
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
public void shutdown() {
group.shutdownGracefully();
}
public static void main(String[] args) throws InterruptedException {
DeviceClient client = new DeviceClient("127.0.0.1", 9000, "DEVICE_0001");
client.connect();
}
}
2. 设备端 Handler(自动重连 + 心跳)
go
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
public class DeviceClientHandler extends ChannelInboundHandlerAdapter {
private final DeviceClient client;
private final String deviceId;
public DeviceClientHandler(DeviceClient client, String deviceId) {
this.client = client;
this.deviceId = deviceId;
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
// 连接成功时发送注册信息
String registerMsg = "REGISTER:" + deviceId;
ctx.writeAndFlush(registerMsg);
System.out.println(deviceId + " -> 注册信息已发送");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
System.out.println(deviceId + " 连接断开,准备重连...");
client.scheduleReconnect(3); // 延迟3秒重连
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
if (evt instanceof IdleStateEvent) {
IdleStateEvent e = (IdleStateEvent) evt;
if (e.state() == IdleState.WRITER_IDLE) {
// 发送心跳包
ctx.writeAndFlush("HEARTBEAT:" + deviceId);
System.out.println(deviceId + " -> 发送心跳");
}
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println(deviceId + " <- 收到消息: " + msg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
System.err.println(deviceId + " 出错: " + cause.getMessage());
ctx.close();
}
}
3. 运行效果
假设你的 Netty 网关监听 9000
端口:
-
设备启动后会连接网关,发送
REGISTER:DEVICE_0001
-
每 30 秒发一次
HEARTBEAT:DEVICE_0001
-
如果连接断开,3 秒后自动重连
-
断网/宕机后自动恢复连接
4. 支持百万连接的做法
-
真实测试要多线程批量创建 DeviceClient,每个模拟一个设备
-
单机 Netty 最大连接数取决于 内存 + 文件描述符限制
-
多台服务器时:
-
-
设备连接时用 Nginx Stream + 一致性 Hash 分发到固定网关
-
网关用 Redis 保存 deviceId → 网关IP 映射
-
后端业务用 Kafka 转发,实现分布式消息处理
-
网关挂了,Nginx 会把连接迁移到其他网关,设备端会自动重连
-