Redis的客户端很多,如Jedis、Lettuce,功能很全,基本支持Redis的所有命令,但是有时业务结合Jedis、Lettuce很是别扭,比如Jedis的同步模式(集群下管道模式支持的不好),Lettuce支持异步,但应用起来不方便,而且性能并不一定会让你满意,优化很困难,这时候,我们可以考虑按自己的需求定制一个Redis客户端,官方对Redis的命令说的很清楚,你可以使用netcat做各种命令测试,也可以用redisCli很方便。下面呢,我们可以自己实现一个队列应用。(代码后续会上传到github)
第一步,选择netty做nio框架
pom.xml引入
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.44.Final</version>
</dependency>
第二步,保存Redis集群的节点信息
Netty的功能这里就不多讲述了,这里列出我们需要解码代码,就是解析Redis的消息
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.TooLongFrameException;
import io.netty.util.ByteProcessor;
import java.util.List;
/**
* @author ZhengBin
* @date 2020/1/7
* 消息解码,可根据具体协议改动
* maxLength:最大长度限制
* minLength:最小长度限制
*/
public class MessageDecoder extends ByteToMessageDecoder {
/**
* redis的命令及返回都是以\n结束
*/
private static final char FIND_R = '\r';
/**
* redis里$后面是长度信息
*/
private static final char TYPE_LEN = '$';
/**
* redis里*后面是信息的条数
*/
private static final char TYPE_NUM = '*';
private final int maxLength;
private final int minLength;
private boolean isValue = false;
private int vLen;
private int skip;
MessageDecoder(final int maxLength, final int minLength) {
this.maxLength = maxLength;
this.minLength = minLength;
}
//通过\n截取
private static int findEndOfLine(final ByteBuf buffer) {
int i = buffer.forEachByte(ByteProcessor.FIND_LF);
if (i > 0 && buffer.getByte(i - 1) == FIND_R) {
i--;
}
return i;
}
@Override
protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
byte[] decoded = decode(ctx, in);
if (decoded != null) {
out.add(decoded);
}
}
private byte[] decode(ChannelHandlerContext ctx, ByteBuf buffer) {
if (isValue) {
final int length = buffer.readableBytes();
if (length >= vLen) {
if (skip == 1) {
isValue = false;
byte[] msg = new byte[vLen];
buffer.readBytes(msg);
return msg;
} else {
skip++;
buffer.skipBytes(vLen);
isValue = false;
return null;
}
} else {
return null;
}
} else {
final int eol = findEndOfLine(buffer);
if (eol >= 0) {
final int length = eol - buffer.readerIndex();
//最大,最小长度限制
if (length > maxLength) {
failMaxError(ctx, maxLength);
return null;
}
if (length < minLength) {
buffer.skipBytes(buffer.getByte(eol) == FIND_R ? 2 : 1);
return null;
}
final int delimLength = buffer.getByte(eol) == FIND_R ? 2 : 1;
byte[] msg = new byte[length];
buffer.readBytes(msg);
buffer.skipBytes(delimLength);
char type = (char) msg[0];
if (type == TYPE_LEN) {
vLen = Integer.parseInt(new String(msg, 1, length - 1));
if (vLen == -1) {
return new byte[0];
}
isValue = true;
return null;
} else if (type == TYPE_NUM) {
skip = 0;
return null;
} else {
return msg;
}
} else {
final int length = buffer.readableBytes();
//最大,最小长度限制
if (length > maxLength) {
failMaxError(ctx, maxLength);
}
return null;
}
}
}
private void failMaxError(final ChannelHandlerContext ctx, int length) {
ctx.fireExceptionCaught(new TooLongFrameException("frame length (" + length + ") exceeds the allowed maximum (" + maxLength + ')'));
}
}
Redis的集群信息,我们可以通过netcat中输入cluster nodes
cluster nodes
$775
d738fb080c07f5aeeb6cafed62f324d47fa73fa5 172.18.1.252:17001@27001 myself,master - 0 1578901250000 21 connected 10922-16383
aa4edbb6ce9e1eb99a017caf0af99bfc3e8468b5 172.18.1.250:17001@27001 master - 0 1578901250618 19 connected 5461-10921
1f0b43d0f8f53a9d7ad736442b41ac5db03ef1a1 172.18.1.251:17000@27000 slave d738fb080c07f5aeeb6cafed62f324d47fa73fa5 0 1578901250118 21 connected
418e29456e0eaf585b3075c24a8d9b52d0dd003b 172.18.1.251:17001@27001 master - 0 1578901249517 20 connected 0-5460
3241213fbc62328c18d65a8a4466ecd181473c54 172.18.1.250:17000@27000 slave 418e29456e0eaf585b3075c24a8d9b52d0dd003b 0 1578901249616 20 connected
7e2d06d07725a7ac2856020b04244ab43b34aafb 172.18.1.252:17000@27000 slave aa4edbb6ce9e1eb99a017caf0af99bfc3e8468b5 0 1578901251118 19 connected
那么我们就可以在链接成功Redis时,同样发送这个指令,下面说一下这个格式,每个节点信息以\n结束,字段是用“ ”分开的,我们关注的信息是ip、port,master(主节点),以及最后一个字段槽范围(后面讲一下这个用途),获取节点信息的代码
import java.util.ArrayList;
import java.util.List;
/**
* @author ZhengBin
* @date 2020/1/13
*/
public class ClusterNodes {
static List<node> clusterNodes = new ArrayList<>();
public static void init(String host, int port, String password) throws Exception {
yl.redis.client.netty.pool.Connection connection = new yl.redis.client.netty.pool.Connection(host, port, password);
String nodesStr = connection.cli("cluster nodes");
String flag = "\n";
for (String clusterNode : nodesStr.split(flag)) {
String[] nodeInfos = clusterNode.split(" ");
if (nodeInfos[2].contains("master")) {
node n = new node();
n.password = password;
n.ip = nodeInfos[1].split(":")[0];
n.port = Integer.parseInt(nodeInfos[1].split(":")[1].split("@")[0]);
n.slot = new int[]{Integer.parseInt(nodeInfos[8].split("-")[0]), Integer.parseInt(nodeInfos[8].split("-")[1])};
clusterNodes.add(n);
}
}
connection.cli("quit");
}
}
第三步,通过crc16算法获取key对应的槽点,并获取相应的节点并建立连接
/**
* @author ZhengBin
* @date 2020/1/9
*/
public class Crc16 {
private static final int[] LOOKUP_TABLE = new int[]{0, 4129, 8258, 12387, 16516, 20645, 24774, 28903, 33032, 37161, 41290, 45419, 49548, 53677, 57806, 61935, 4657, 528, 12915, 8786, 21173, 17044, 29431, 25302, 37689, 33560, 45947, 41818, 54205, 50076, 62463, 58334, 9314, 13379, 1056, 5121, 25830, 29895, 17572, 21637, 42346, 46411, 34088, 38153, 58862, 62927, 50604, 54669, 13907, 9842, 5649, 1584, 30423, 26358, 22165, 18100, 46939, 42874, 38681, 34616, 63455, 59390, 55197, 51132, 18628, 22757, 26758, 30887, 2112, 6241, 10242, 14371, 51660, 55789, 59790, 63919, 35144, 39273, 43274, 47403, 23285, 19156, 31415, 27286, 6769, 2640, 14899, 10770, 56317, 52188, 64447, 60318, 39801, 35672, 47931, 43802, 27814, 31879, 19684, 23749, 11298, 15363, 3168, 7233, 60846, 64911, 52716, 56781, 44330, 48395, 36200, 40265, 32407, 28342, 24277, 20212, 15891, 11826, 7761, 3696, 65439, 61374, 57309, 53244, 48923, 44858, 40793, 36728, 37256, 33193, 45514, 41451, 53516, 49453, 61774, 57711, 4224, 161, 12482, 8419, 20484, 16421, 28742, 24679, 33721, 37784, 41979, 46042, 49981, 54044, 58239, 62302, 689, 4752, 8947, 13010, 16949, 21012, 25207, 29270, 46570, 42443, 38312, 34185, 62830, 58703, 54572, 50445, 13538, 9411, 5280, 1153, 29798, 25671, 21540, 17413, 42971, 47098, 34713, 38840, 59231, 63358, 50973, 55100, 9939, 14066, 1681, 5808, 26199, 30326, 17941, 22068, 55628, 51565, 63758, 59695, 39368, 35305, 47498, 43435, 22596, 18533, 30726, 26663, 6336, 2273, 14466, 10403, 52093, 56156, 60223, 64286, 35833, 39896, 43963, 48026, 19061, 23124, 27191, 31254, 2801, 6864, 10931, 14994, 64814, 60687, 56684, 52557, 48554, 44427, 40424, 36297, 31782, 27655, 23652, 19525, 15522, 11395, 7392, 3265, 61215, 65342, 53085, 57212, 44955, 49082, 36825, 40952, 28183, 32310, 20053, 24180, 11923, 16050, 3793, 7920};
public static int getCrc16(byte[] bytes) {
int crc = 0;
for (byte aByte : bytes) {
crc = crc << 8 ^ LOOKUP_TABLE[(crc >>> 8 ^ aByte & 255) & 255];
}
return crc & '\uffff' & 16383;
}
}
import yl.redis.client.netty.util.Crc16;
/**
* @author ZhengBin
* @date 2020/1/13
*/
public class Queue {
public static void add(String key, CallBack cb) {
int slot = Crc16.getCrc16(key.getBytes());
for (node n : ClusterNodes.clusterNodes) {
if (slot >= n.slot[0] && slot <= n.slot[1]) {
try {
new Connection(key, n, cb);
} catch (Exception e) {
e.printStackTrace();
}
break;
}
}
}
}
说一说为什么用crc16,因为Redis集群通过key的crc16算法值为槽点,Redis集群的节点负责不同的槽点0-5461这样,所以我们的队列取值就有讲究了,最好是分配到不同的节点上,这样性能最好
第四步,做一个业务接口,传给netty的Handler,并在Handler回调业务(业务一定要非阻塞,不然会影响netty性能)
/**
* @author ZhengBin
* @date 2020/1/13
*/
public interface CallBack {
/**
* 回调方法
*
* @param v 返回的内容
*/
void call(String v);
}
import yl.redis.client.netty.queue.CallBack;
/**
* @author ZhengBin
* @date 2020/1/13
*/
public class TestListA implements CallBack {
@Override
public void call(String v) {
if (!"+OK".equals(v)) {
System.out.println(v + ":" + System.currentTimeMillis());
}
}
}
至此思路讲完,谈一谈回调方法不阻塞,这个问题大多是用数程池,但是我觉得还是用一些异步框架好,比如:http调用的话用httpasyncclient。