Redis 客户端
客户端通信原理
客户端和服务器通过 TCP 连接来进行数据交互,服务器默认的端口号为 6379
客户端和服务器发送的命令或数据一律以\r\n(CRLF 回车+换行)结尾
如果使用 wireshark 对 jedis 抓包
环境:Jedis
连接到虚拟机 202,运行 main,对 VMnet8 抓包
过滤条件:ip.dst==192.168.8.202 and tcp.port in {6379}
set qingshan 抓包:
可以看到实际发出的数据包是:
*3\r\n$3\r\nSET\r\n$8\r\nqingshan\r\n$4\r\n2673\r\n
get qingshan 抓包:
*2\r\n$3\r\nGET\r\n$8\r\nqingshan\r\n
客户端跟 Redis 之间使用一种特殊的编码格式(在 AOF 文件里面我们看到了),叫做 Redis Serialization Protocol(Redis 序列化协议).特点:容易实现,解析快,可读性强.客户端发给服务端的消息需要经过编码,服务端收到之后会按约定进行解码,反之亦然
基于此,我们可以自己实现一个 Redis 客户端
参考:myclient.MyClient.java
- 建立 Socket 连接
- OutputStream 写入数据(发送到服务端)
- InputStream 读取数据(从服务端接口)
基于这种协议,我们可以用 Java 实现所有的 Redis 操作命令.当然,我们不需要这么做,因为已经有很多比较成熟的 Java 客户端,实现了完整的功能和高级特性,并且提供了良好的性能
https://redis.io/clients#java
官网推荐的 Java 客户端有 3 个 Jedis,Redisson 和 Luttuce
客户端 |
描述 |
Jedis |
Ablazinglysmallandsaneredisjavaclient |
lettuce |
AdvancedRedisclientforthread-safesync,async,andreactiveusage.SupportsCluster,Sentinel,Pipelining,andcodecs |
Redisson |
distributedandscalableJavadatastructuresontopofRedisserver |
Spring 连接 Redis 用的是什么?RedisConnectionFactory 接口支持多种实现,例如:JedisConnectionFactory,JredisConnectionFactory,LettuceConnectionFactory,SrpConnectionFactory
Jedis
https://github.com/xetorthio/jedis
特点
Jedis 是我们最熟悉和最常用的客户端.轻量,简洁,便于集成和改造
BasicTest.java
public static void main(String[] args){
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.set("qingshan", "2673");
System.out.println(jedis.get("qingshan"));
jedis.close();
}
Jedis 多个线程使用一个连接的时候线程不安全.可以使用连接池,为每个请求创建不同的连接,基于 Apachecommonpool 实现.跟数据库一样,可以设置最大连接数等参数.Jedis 中有多种连接池的子类
例如:ShardingTest.java
public static void main(String[] args){
JedisPool pool = new JedisPool(ip, port);
Jedis jedis = jedisPool.getResource();
}
Jedis 有 4 种工作模式:单节点,分片,哨兵,集群
3 种请求模式:Client,Pipeline,事务.Client 模式就是客户端发送一个命令,阻塞等待服务端执行,然后读取返回结果.Pipeline 模式是一次性发送多个命令,最后一次取回所有的返回结果,这种模式通过减少网络的往返时间和 io 读写次数,大幅度提高通信性能.第三种是事务模式.Transaction 模式即开启 Redis 的事务管理,事务模式开启后,所有的命令(除了 exec,discard,multi 和 watch)到达服务端以后不会立即执行,会进入一个等待队列
Sentinel 获取连接原理
问题:Jedis 连接 Sentinel 的时候,我们配置的是全部哨兵的地址.Sentinel 是如何返回可用的 master 地址的呢?
在构造方法中:
pool = new JedisSentinelPool(masterName, sentinels);
调用了:
HostAndPort master = initSentinels(sentinels, masterName);
查看:
private HostAndPort initSentinels(Set sentinels, final String masterName){
HostAndPort master = null;
boolean sentinelAvailable = false;
log.info("Trying to find master from available Sentinels...");
// 有多个 sentinels,遍历这些个 sentinels
for (String sentinel : sentinels){
// host:port 表示的 sentinel 地址转化为一个 HostAndPort 对象
final HostAndPort hap = HostAndPort.parseString(sentinel);
log.fine("Connecting to Sentinel " + hap);
Jedis jedis = null;
try {
// 连接到 sentinel
jedis = new Jedis(hap.getHost(), hap.getPort());
// 根据 masterName 得到 master 的地址,返回一个 list,host= list[0], port =// list[1]
List masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
// connected to sentinel...
sentinelAvailable = true;
if (masterAddr == null || masterAddr.size()!= 2){
log.warning("Can not get master addr, master name: " + masterName + ". Sentinel: " + hap + ".");
continue;
}
// 如果在任何一个 sentinel 中找到了 master,不再遍历 sentinels
master = toHostAndPort(masterAddr);
log.fine("Found Redis master at " + master);
break;
} catch (JedisException e){
// resolves #1036, it should handle JedisException there's another chance
// of raising JedisDataException
log.warning("Cannot get master address from sentinel running @ " + hap + ". Reason: " + e + ". Trying next one.");
} finally {
if (jedis != null){
jedis.close();
}
}
}
// 到这里,如果 master 为 null,则说明有两种情况,一种是所有的 sentinels 节点都 down 掉了,一种是 master 节点没有被存活的 sentinels 监控到
if (master == null){
if (sentinelAvailable){
// can connect to sentinel, but master name seems to not
// monitored
throw new JedisException("Can connect to sentinel, but " + masterName + " seems to be not monitored...");
} else {
throw new JedisConnectionException("All sentinels down, cannot determine where is " + masterName + " master is running...");
}
}
// 如果走到这里,说明找到了 master 的地址
log.info("Redis master running at " + master + ", starting Sentinel listeners...");
// 启动对每个 sentinels 的监听为每个 sentinel 都启动了一个监听者 MasterListener
// MasterListener 本身是一个线 程,它会去订阅 sentinel 上关于 master 节点地址改变的消息
for (String sentinel : sentinels){
final HostAndPort hap = HostAndPort.parseString(sentinel);
MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
// whether MasterListener threads are alive or not, process can be stopped
masterListener.setDaemon(true);
masterListeners.add(masterListener);
masterListener.start();
}
return master;
}
Cluster 获取连接原理
问题:使用 Jedis 连接 Cluster 的时候,我们只需要连接到任意一个或者多个 redis group 中的实例地址,那我们是怎么获取到需要操作的 Redis Master 实例的?
关键问题:在于如何存储 slot 和 Redis 连接池的关系
- 程序启动初始化集群环境,读取配置文件中的节点配置,无论是主从,无论多少个,只拿第一个,获取 redis 连接实例(后面有个 break)
// redis.clients.jedis.JedisClusterConnectionHandler#initializeSlotsCache
private void initializeSlotsCache(Set startNodes, GenericObjectPoolConfig poolConfig, String password){
for (HostAndPort hostAndPort : startNodes){
// 获取一个 Jedis 实例
Jedis jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort());
if (password != null){
jedis.auth(password);
}
try {
// 获取 Redis 节点和 Slot 虚拟槽
cache.discoverClusterNodesAndSlots(jedis);
// 直接跳出循环
break;
} catch (JedisConnectionException e){
// try next nodes
} finally {
if (jedis != null){
jedis.close();
}
}
}
}
-
用获取的 redis 连接实例执行 clusterSlots()方法,实际执行 redis 服务端 clusterslots 命令,获取虚拟槽信息
该集合的基本信息为[long,long,List,List],第一,二个元素是该节点负责槽点的起始位置,第三个元素是主节点信息,第四个元素为主节点对应的从节点信息.该 list 的基本信息为[string,int,string],第一个为 host 信息,第二个为 port 信息,第三个为唯一 id
这里 7296 是 7291 的 slave 节点
- 获取有关节点的槽点信息后,调用 getAssignedSlotArray(slotinfo)来获取所有的槽点值
- 再获取主节点的地址信息,调用 generateHostAndPort(hostInfo)方法,生成一个 ostAndPort 对象
- 再根据节点地址信息来设置节点对应的 JedisPool,即设置 Map nodes 的值
接下来判断若此时节点信息为主节点信息时,则调用 assignSlotsToNodes 方法,设置每个槽点值对应的连接池,即设置 Map slots 的值
// redis.clients.jedis.JedisClusterInfoCache#discoverClusterNodesAndSlots
public void discoverClusterNodesAndSlots(Jedis jedis){
w.lock();
try {
reset();
// 获取节点集合
List