Redis三种客户端介绍及手写一个客户端

1,317 阅读8分钟

前言

官网推荐的Java客户端有3个,地址:redis.io/clients ,标黄色五角星的是推荐的。

image.png

image.png

Spring 操作 Redis 提供了一个模板方法 RedisTemplate,这个是不是Spring官方开发的一个客户端呢?其实不是,Spring定义了一个连接工厂接口,RedisConnectionFactory,这个接口有很多实现,例如:JedisConnectionFactory、JredisConnectionFactory、LettuceConnectionFactory、SrpConnectionFactory。

也就是说RedisTemplate对其他现成的客户端再进行了一层封装而已,在Spring Boot2.x版本之前,RedisTemplate默认使用Jedis,2.x版本之后默认使用Lettuce。所以我们学习的底层最终还是这三个客户端

本文源码在:github.com/xuhaoj/redi…

Jedis

基本用法

官网地址,github.com/redis/jedis ,Jedis是我们最熟悉和最常用的客户端,如果不用RedisTemplate 的话可以直接创建 Jedis 的连接。

    public static void main(String[] args) {
        Jedis jedis = new Jedis("39.103.144.86", 6379);
        //如果设置密码的话
        //jedis.auth("xushuaige");
        jedis.set("jackxu", "shuaige");
        System.out.println(jedis.get("jackxu"));
        jedis.close();
    }

源码在com/xhj/jedis/client/BasicTest.java

连接池

但是使用 Jedis 有一个问题,多个线程使用一个连接的时候线程是不安全的。 这时候有个解决办法就是使用连接池,为每个请求创建不同的连接,基于Apache common pool实现。

Jedis 的连接池有三个实现:JedisPool、ShardedJedisPool、JedisSentinelPool,都是用 getResource 从连接池获取一个连接。

     /**
     * 普通连接池
     */
    public static void ordinaryPool() {
        JedisPool pool = new JedisPool("39.103.144.86", 6379);
        Jedis jedis = pool.getResource();
        jedis.set("jackxu", "帅哥");
        System.out.println(jedis.get("jackxu"));
    }

    /**
     * 分片连接池
     */
    public static void shardedPool() {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        
        // Redis服务器
        JedisShardInfo shardInfo1 = new JedisShardInfo("192.168.44.181", 6379);

        // 连接池
        List<JedisShardInfo> infoList = Arrays.asList(shardInfo1);
        ShardedJedisPool jedisPool = new ShardedJedisPool(poolConfig, infoList);

        ShardedJedis jedis = jedisPool.getResource();
        jedis.set("jackxu", "分片测试");
        System.out.println(jedis.get("jackxu"));
    }

    /**
     * 哨兵连接池
     */
    public static void sentinelPool() {
        String masterName = "redis-master";
        Set<String> sentinels = new HashSet<String>();
        sentinels.add("192.168.44.186:26379");
        sentinels.add("192.168.44.187:26379");
        sentinels.add("192.168.44.188:26379");

        JedisSentinelPool pool = new JedisSentinelPool(masterName, sentinels);
        pool.getResource().set("jackxu", "哨兵" + System.currentTimeMillis() + "连接池");
        System.out.println(pool.getResource().get("jackxu"));
    }

Jedis 的功能比较完善,Redis官方的特性全部支持,比如发布订阅、事务、Lua脚本、客户端分片、哨兵、集群、pipeline等等。

Jedis 连接 Sentinel 需要配置所有的哨兵地址,Cluster 连接哨兵只需要配置任何一个 master 或者 slave的地址就可以了。

分布式锁

用Jedis实现分布式锁,大家可以看下 《介绍几种常见的分布式锁写法》,这篇文章也是我写的。

Pipeline

官网介绍:redis.io/topics/pipe…

我们平时说 Redis 是单线程的,说的是 Redis 的请求是单线程处理的,只有上一个命令的相应结束以后,下一个命令才会去处理。如果一次要操作10万个Key,客户端跟服务端就要交互10万次,排队的时间加上网络通信的时间,就会非常的慢。

怎么解决这个问题呢?跟Kafka、MySQL类似,把一组命令组装在一起然后发给Redis的服务端执行,然后一次性获得返回的结果,这个就是pipeline。pipeline通过一个队列把所有的命令缓存起来,然后把多个命令在一次连接中发送给服务器。

下面我们做实验来测试一下,使用 pipeline 的方式在 get 和 set 的时候都要使用 pipeline 的写法。先写set 的对比:

    public static void main(String[] args) {
        Jedis jedis = new Jedis("39.103.144.86", 6379);
        long t1 = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            jedis.set("commonBatch" + i, "" + i);
        }
        long t2 = System.currentTimeMillis();
        System.out.println("普通set耗时:" + (t2 - t1) + "ms");


        Pipeline pipelined = jedis.pipelined();
        long t3 = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            pipelined.set("pipelineBatch" + i, "" + i);
        }
        pipelined.syncAndReturnAll();
        long t4 = System.currentTimeMillis();
        System.out.println("pipeline set耗时:" + (t4 - t3) + "ms");
    }

普通的 set 我等了几分钟都没好,实在等不及就关掉了,感兴趣的小伙伴可以试下到底用多长时间,而 pipeline 的方式只用了4秒!

image.png

接下来测试 get 的速度

    public static void main(String[] args) {
        new Thread(() -> {
            Jedis jedis = new Jedis("39.103.144.86", 6379);
            Set<String> keys = jedis.keys("pipelineBatch*");
            List<String> result = new ArrayList();
            long t1 = System.currentTimeMillis();
            for (String key : keys) {
                result.add(jedis.get(key));
            }
            for (String src : result) {
                //System.out.println(src);
            }
            System.out.println("直接get耗时:" + (System.currentTimeMillis() - t1));
        }).start();


        new Thread(() -> {
            Jedis jedis = new Jedis("39.103.144.86", 6379);
            Set<String> keys = jedis.keys("pipelineBatch*");
            List<Object> result = new ArrayList();
            Pipeline pipelined = jedis.pipelined();
            long t1 = System.currentTimeMillis();
            for (String key : keys) {
                pipelined.get(key);
            }
            result = pipelined.syncAndReturnAll();
            for (Object src : result) {
                //System.out.println(src);
            }
            System.out.println("Pipeline get耗时:" + (System.currentTimeMillis() - t1));
        }).start();
    }

普通的 get 依然卡主了,而 pipeline 方式的 get 仅用了14秒。 image.png

jedis-pipeline 的 client-buffer 的限制是 8192bytes,客户端堆积的命令超过8M时,会发送给服务端。pipeline 对于条数没有限制,但是命令可能会受限于 TCP 包的大小。如果某些操作需要马上得到 redis 操作结果的这种场景不适合用 pipeline,对于实时性和成功性要求不高的可以用 pipeline。

源码在com/xhj/jedis/pipeline/PipelineGet.java、com/xhj/jedis/pipeline/PipelineSet.java

Lettuce

官网地址:lettuce.io ,lettuce是生菜、莴苣的意思,与 Jedis 相比,Lettuce 则完全克服了线程不安全的缺点。Lettuces 是一个可伸缩的线程安全的Redis客户端,支持同步、异步和响应式模式(Reactive)。

Lettuce基于 Netty 框架构建,支持 Redis 的全部高级功能,如发布订阅、事务、Lua脚本、 Sentinel、集群、Pipeline支持连接池。

同步调用

    public static void main(String[] args) {
        // 创建客户端
        RedisClient client = RedisClient.create("redis://39.103.144.86:6379");
        // 线程安全的长连接,连接丢失时会自动重连
        StatefulRedisConnection<String, String> connection = client.connect();
        // 获取同步执行命令,默认超时时间为 60s
        RedisCommands<String, String> sync = connection.sync();
        // 发送get请求,获取值
        sync.set("jackxu", "shuaige");
        String value = sync.get("jackxu");
        System.out.println(value);
        //关闭连接
        connection.close();
        //关掉客户端
        client.shutdown();
    }

代码在com/xhj/lettuce/LettuceSyncTest.java

异步调用

异步的结果使用 RedisFuture 包装,提供了大量的回调的方法

    public static void main(String[] args) {
        RedisClient client = RedisClient.create("redis://39.103.144.86:6379");
        // 线程安全的长连接,连接丢失时会自动重连
        StatefulRedisConnection<String, String> connection = client.connect();
        // 获取异步执行命令api
        RedisAsyncCommands<String, String> commands = connection.async();
        // 获取RedisFuture<T>
        commands.set("jackxu", "shuaige");
        RedisFuture<String> future = commands.get("jackxu");
        try {
            String value = future.get(60, TimeUnit.SECONDS);
            System.out.println(value);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

代码在com/xhj/lettuce/LettuceASyncTest.java

RedisTemplate

开头的时候介绍了 Lettuce 是 Spring Boot2.x 的默认客户端,替换了 Jedis。集成之后我们不需要单独使用它,直接调用 Spring 的 RedisTemplate 操作即可,连接的创建和关闭也不需要我们关心。

下面我们看下怎么使用,先配置下RedisTemplate。

@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisConfig {
    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(
            RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        //使用fastjson序列化
        FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
        // value值的序列化采用fastJsonRedisSerializer
        template.setValueSerializer(fastJsonRedisSerializer);
        template.setHashValueSerializer(fastJsonRedisSerializer);
        // key的序列化采用StringRedisSerializer
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}

使用的时候注入下,get set用法很简单。

    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    //set
    redisTemplate.opsForValue().set(key, value);
    //get
    redisTemplate.opsForValue().get(key);

代码在com/xhj/lettuce/RedisConfig.java、com/xhj/lettuce/RedisUtil.java

Redisson

官网:redisson.orggithub.com/redisson/re…

介绍

Redisson 是一个在 Redis 的基础上实现的 Java 主内存数据网络(In-Memory Data Grid),提供了分布式和可扩展的Java数据结构,比如分布式的 Map、List、Queue、Set,不需要自己去运行一个服务实现。

基于 Netty 实现,采用非阻塞 IO,性能高;支持异步请求。支持连接池、pipeline、LUA Scripting、Redis Sentinel、Redis Cluster;不支持事务,官方建议以 LUA Scripting 代替事务;主从、哨兵、集群都支持。Spring 也可以配置和注入 RedissonClient。

分布式锁

还是见《介绍几种常见的分布式锁写法》 这篇文章。

Redisson 跟 Jedis 的定位不同,它不是一个单纯的 Redis 客户端,而是基于 Redis 实现的分布式服务,可以实现一些分布式的数据结构。

手写客户端

前面介绍了三种常见的、热门的客户端,我们有一个想法,能不能自己手写一个客户端呢?答案是可以的。

首先我们知道 Redis 默认监听 6379 的端口号,可以通过 TCP 的方式建立连接,在 Java 中我们可以通过Socket 的方式来进行 TCP 间的通信。其次是与 Redis 服务端之间进行通信使用的是 RESP 协议(Redis Serialization Protocol),这是一种特殊的消息格式,每个命令都是以\r\n(回车+换行)结尾。

发消息或者响应消息需要按这种格式编码,接收消息需要按这种格式解码。这种编码格式在AOF文件中也是这么做的。下面我们通过抓包工具来具体看下这个协议长什么样。

抓包

首先用抓包工具过滤条件ip.dst==39.103.144.86 and tcp.port in {6379},然后用 Jedis 执行一下get set的命令

image.png

执行一下,打开抓包工具

image.png

我们看下set命令

image.png

实际发出的数据包是

这里的*3代表三个参数 set jackxu shuaige,$3代表set是三个字符,$6代表jackxu是六个字符,$7代表shuaige是七个字符
*3\r\n$3\r\nSET\r\n$6\r\njackxu\r\n$7\r\nshuaige

再来看下get命令

image.png

实际发出的数据包是

这里的*2代表三个参数 get jackxu,$3代表get是三个字符,$6代表jackxu是六个字符
*2\r\n$3\r\nGET\r\n$6\r\njackxu

其实就是把命令、参数和它们的长度用\r\n连接起来。

手写客户端

熟悉完上面的原理后,我们就可以开始手写客户端了。

  1. 建立Socket连接
  2. OutputStream写入数据(发送命令到服务端)
  3. InputStream读取数据(从服务端接收数据)
/**
 * @author jackxu
 */
public class MyClient {

    private Socket socket;
    private OutputStream write;
    private InputStream read;

    public MyClient(String host, int port) throws IOException {
        socket = new Socket(host, port);
        write = socket.getOutputStream();
        read = socket.getInputStream();
    }

    /**
     * 实现了set方法
     *
     * @param key
     * @param val
     * @throws IOException
     */
    public void set(String key, String val) throws IOException {
        StringBuffer sb = new StringBuffer();
        // 代表3个参数(set key value)
        sb.append("*3").append("\r\n");
        // 第一个参数(set)的长度
        sb.append("$3").append("\r\n");
        // 第一个参数的内容
        sb.append("SET").append("\r\n");

        // 第二个参数key的长度(不定,动态获取)
        sb.append("$").append(key.getBytes().length).append("\r\n");
        // 第二个参数key的内容
        sb.append(key).append("\r\n");
        // 第三个参数value的长度(不定,动态获取)
        sb.append("$").append(val.getBytes().length).append("\r\n");
        // 第三个参数value的内容
        sb.append(val).append("\r\n");

        // 发送命令
        write.write(sb.toString().getBytes());
        byte[] bytes = new byte[1024];
        // 接收响应
        read.read(bytes);
        System.out.println("set response:" + new String(bytes));
    }

    /**
     * 实现了get方法
     *
     * @param key
     * @throws IOException
     */
    public void get(String key) throws IOException {
        StringBuffer sb = new StringBuffer();
        // 代表2个参数
        sb.append("*2").append("\r\n");
        // 第一个参数(get)的长度
        sb.append("$3").append("\r\n");
        // 第一个参数的内容
        sb.append("GET").append("\r\n");

        // 第二个参数key的长度
        sb.append("$").append(key.getBytes().length).append("\r\n");
        // 第二个参数内容
        sb.append(key).append("\r\n");

        write.write(sb.toString().getBytes());
        byte[] bytes = new byte[1024];
        read.read(bytes);
        System.out.println("get response:" + new String(bytes));
    }

    public static void main(String[] args) throws IOException {
        MyClient client = new MyClient("39.103.144.86", 6379);
        client.set("myclient", "666");
        client.get("myclient");
    }
}

代码在com/xhj/myclient/MyClient.java

执行一下,成功 set 和 get 到值了! image.png

结尾

最后的手写一个客户端相当于是底层原理,知识扩展,就像我们手写一个tomcat,手写一个RPC框架,都是一个简易版的东西,便于我们的理解,但是不能用到生产哦。

本文介绍的三种客户端有的介绍比较详细,有的介绍比较简单,具体选型和具体场景大家在使用过程中还需要去查找官网,本文只能说是一个初级入门科普贴,最后感谢大家的观看!