按功能定制自己的Redis客户端

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。

源码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值