RESP(REdis Serialization Protocol )协议:
redis客户端和服务端是通过RESP协议来进行交互的,RESP协议是基于TCP协议的,将客户端命令以某种形式传递给服务端,服务端接收后,进行命令的解析,并执行。这里我们先将redis持久化方式设置为AOF模式,准确查看RESP协议是怎么进行传输的。将appendonly.aof文件清空,然后使用set name xiaoming命令, 刷新文件,可以看到文件上如下字符,且每行以'\r\n结尾':
*3 //表明有几个参数
$3 //第一个参数的长度为3
SET //第一个参数
$4 //第二个参数的字符为4
name //第二个参数
$4 //第三个参数的长度
quan //第三个参数
知道了RESP的大致格式后,我们可以使用虚拟服务来进行客户端请求的拦截。
首先先建虚拟服务器:
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(6378);
Socket socket = server.accept();
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String data = null;
while((data=reader.readLine()) != null) {
System.out.println(data);
}
}
然后使用Jedis新建客户端:
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6378);
jedis.set("name", "xiaoming");
jedis.close();
}
控制台输出与appendonly.aof文件输出相同。
接下来我们自己写一个redis交互的类,仿照Jedis。这里我们使用的是长连接,即可以保持长时间的连接,而不是每请求一次就新建一个连接。
package jedis;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class CustomClient {
InputStream is;
OutputStream os;
Socket socket;
public CustomClient() throws IOException {
socket = new Socket("localhost", 6379);
is = socket.getInputStream();
os = socket.getOutputStream();
}
public void set(String key ,String value) {
StringBuilder builder = new StringBuilder();
builder.append("*3").append("\r\n");
builder.append("$3").append("\r\n");
builder.append("SET").append("\r\n");
builder.append("$").append(key.length()).append("\r\n");
builder.append(key).append("\r\n");
builder.append("$").append(value.length()).append("\r\n");
builder.append(value).append("\r\n");
try {
os.write(builder.toString().getBytes());
byte[] data = new byte[1024];
is.read(data);
System.out.println(new String(data));
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
CustomClient client;
try {
client = new CustomClient();
client.set("name", "daming");
} catch (IOException e) {
e.printStackTrace();
}
}
}
查看键值对,可以看到name已经被修改为daming;
PipeLine的实现
TCP是请求响应模型,即发出一次请求,获取一次响应,但是这样数据间传输比较缓慢,在redis中使用pipeLine来解决这个难题。解决办法是:我们发起多次请求,不必关心响应结果,这样redis会将我们每次的响应结果缓存到内存中,等请求全部执行完成后,再从内存中获取响应结果。
首先我们定义一个PipeLine类,set方法或自定义客户端的set方法类似,只是没有了获取响应结果。response方法返回我们的响应结果。我们可以使用for循环来进行执行时间的比较,可以看到使用管道后速度明显加快。
public class PipeLine {
InputStream is;
OutputStream os;
public PipeLine(InputStream is,OutputStream os) {
this.is = is;
this.os = os;
}
public void set(String key ,String value) {
StringBuilder builder = new StringBuilder();
builder.append("*3").append("\r\n");
builder.append("$3").append("\r\n");
builder.append("SET").append("\r\n");
builder.append("$").append(key.length()).append("\r\n");
builder.append(key).append("\r\n");
builder.append("$").append(value.length()).append("\r\n");
builder.append(value).append("\r\n");
try {
os.write(builder.toString().getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
public String response() {
byte[] data = new byte[1024*100];
try {
is.read(data);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return new String(data);
}
}
订阅模式的实现
创建SubScribe类,来实现订阅。
public class SubScribe {
InputStream is;
OutputStream os;
public SubScribe(InputStream is,OutputStream os) {
this.is = is;
this.os = os;
}
//模仿订阅通道
public void sub(String channel) {
StringBuilder builder = new StringBuilder();
builder.append("*2").append("\r\n");
builder.append("$9").append("\r\n");
builder.append("SUBSCRIBE").append("\r\n");
builder.append("$").append(channel.length()).append("\r\n");
builder.append(channel).append("\r\n");
try {
os.write(builder.toString().getBytes());
byte[] data = new byte[1024];
//使用for循环来进行订阅消息的接收
while(true) {
is.read(data);
System.out.println(new String(data));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
redis代理
通常我们只有一个redis实例,但是会有局限性:不方便扩容;复制数据比较麻烦,因此我们可以使用多个实例,也就是集群。这里我们采用的负载均衡。负载均衡即按照负载策略均匀分布在不同的实例中。
负载策略是一种算法,将数据均匀分布的一种算法。
这里我们使用的负载策略是对key的长度进行取模。代理类代码如下:
通过创建虚拟服务器,监听该端口,对每个连接该服务的socket的输入流进行解析,获取key的长度,创建不同的客户端进行命令的执行,最后关闭客户端。
public class RedisProxy {
private static List<Config> list;
static {
list = new ArrayList<>();
list.add(new Config("localhost", 6379));
list.add(new Config("localhost", 6380));
list.add(new Config("localhost", 6381));
}
//负载算法实现
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(9999);
// 监听端口
Socket socket ;
while((socket = server.accept())!=null) {
System.out.println("进行负载算法");
byte[] data = new byte[1024];
InputStream is = socket.getInputStream();
int length = is.read(data);
String result = new String(data);
//获取到key值的长度
String length = result.split("\r\n")[3].split("\\$")[1];
int index = Integer.parseInt(key)%list.size();
Config config = list.get(index);
Socket client = new Socket(config.getHost(), config.getPort());
client.getOutputStream().write(data,0,length);
byte[] res = new byte[1024*10];
client.getInputStream().read(res);
System.out.println(new String(res));
client.close();
socket.getOutputStream().write(res); //需要将响应结果填充到jedis的输出域中,否则会报错。
}
}
static class Config {
private String host;
private int port;
public Config(String host, int port) {
this.host = host;
this.port = port;
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
}
}
测试类如下:
使用的是相通的端口9999。
public static void main(String[] args) {
Jedis j1 = new Jedis("localhost", 9999);
j1.set("a", "a");
Jedis j2 = new Jedis("localhost", 9999);
j2.set("ab", "ab");
Jedis j3 = new Jedis("localhost", 9999);
j3.set("abc", "abc");
}
当然,在实际开发中,我们是不会自己写代理的,难以维护,使用成熟的代理服务。例如:
Codis——豌豆荚开源的Redis分布式中间件