4.2、Java客户端Jedis

Java客户端Jedis

Java有很多优秀的Redis客户端(详见:http://redis.io/clients#java ),
这里介绍使用较为广泛的客户端Jedis,本节将按照以下几个方面对Jedis进行介
绍:

- 获取Jedis
- Jedis的基本使用
- Jedis连接池使用
- Jedis中Pipleline使用
- Jedis的Lua脚本使用
  1. 获取Jedis

    Jedis属于Java的第三方开发包,在Java中获取第三方开发包通常有两种方式:

    • 直接下载目标版本的Jedis-${version}.jar包加入到项目中。
    • 使用集成构建工具,例如maven、gradle等将Jedis目标版本的配置加入到项
      目中。

    通常在实际项目中使用第二种方式,但如果只是想测试一下Jedis,第一种方法
    也是可以的。在写本书时,Jedis最新发布的稳定版本2.8.2,以Maven为例
    子,在项目中加入下面的依赖即可:

    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.8.2</version>
    </dependency>
    

    对于第三方开发包,版本的选择也是至关重要的,因为Redis更新速度比较快,
    如果客户端跟不上服务端的速度,有些特性和bug不能及时更新,不利于日常开
    发。通常来讲选取第三方开发包有如下两个策略:

    • 选择比较稳定的版本,也就是尽可能选择稳定的里程碑版本,这些版本已经经
      过多次alpha,beta的修复,基本算是稳定了。

    • 选择更新活跃的第三方开发包,例如Redis3.0有了Redis Cluster新特性,
      但是如果使用的客户端一直不支持,并且维护的人也比较少,这种就谨慎选择。

    本节介绍的Jedis基本满足上述两个特点,下面将对Jedis的基本使用方法进行
    介绍。

  2. Jedis的基本使用方法

    Jedis的使用方法非常简单,只要下面三行代码就可以实现get功能:

    # 1. 生成一个Jedis对象,这个对象负责和指定Redis实例进行通信
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    # 2. jedis执行set操作
    jedis.set("hello", "world");
    # 3. jedis执行get操作,value="world"
    String value = jedis.get("hello");
    

    可以看到初始化Jedis需要两个参数:Redis实例的IP和端口,除了这两个参数
    外,还有一个包含了四个参数的构造函数是比较常用的:

    Jedis(final String host, final int port, final int connectionTimeout, final int soTImeout)
    

    参数说明:

    • host:Redis实例的所在机器的IP。
    • port:Redis实例的端口。
    • connectionTimeout:客户端连接超时。
    • soTimeout:客户端读写超时。

    如果想看一下执行如果:

    String setResult = jedis.set("hello", "world");
    String getResult = jedis.get("hello");
    System.out.println(setResult);
    System.out.println(getResult);
    

    输入结果为:

    OK
    world
    

    可以看到jedis.set的返回结果是OK,和redis-cli的执行效果是一样的,只不
    过结果类型变为了Java的数据类型。上面的这种写法只是为了演示使用,在实
    际项目中比较推荐使用try catch finally的形式来进行来进行代码的书写:
    一方面可以在Jedis出现异常的时候(本身是网络操作),将异常进行捕获或者
    抛出;另一个方面无论执行成功或者失败,将Jedis连接关闭掉,在开发中关闭
    不用的连接资源是一种好的习惯,代码类似如下:

    Jedis jedis = null;
    try{
        jedis = new Jedis("127.0.0.1", 6379);
        jedis.get("hello");
    }catch (Exception e){
        logger.error(e.getMessage(), e);
    }finally{
        if(jedis != null){
            jedis.close();
        }
    }
    

    下面用一个例子说明Jedis对于Redis五种数据结构的操作,为了节省篇幅,所
    有返回结果放在注释中。

    //1.string
    //输出结果:OK
    jedis.set("hello", "world");
    //输出结果:world
    jedis.get("hello");
    //输出结果:1
    jedis.incr("counter");
    
    //2.hash
    jedis.hset("myhash", "f1", "v1");
    jedis.hset("myhash", "f2", "v2");
    //输出结果:{f1=v1, f2=v2}
    jedis.hgetAll("myhash");
    
    //3.list
    jedis.rpush("mylist", "1");
    jedis.rpush("mylist", "2");
    jedis.rpush("mylist", "3");
    //输出结果:{1, 2, 3}
    jedis.lrange("mylist", 0, -1);
    
    //4.set
    jedis.sadd("myset", "a");
    jedis.sadd("myset", "b");
    jedis.sadd("myset", "a");
    //输出结果:{b, a}
    jedis.smemebers{b, a};
    
    //5.zset
    jedis.zadd("myzset", 99, "tom");
    jedis.zadd("myzset", 66, "peter");
    jedis.zadd("myzset", 33, "james");
    //输出结果:[[["james"], 33.0], [["peter"], 66.0], [["tom"],99.0]]
    jedis.zrangeWithScores("myset", 0, -1);
    

    参数除了可以是字符串,Jedis还提供了字节数组的参数,例如:

    public String set(final String key, String value)
    public String set(final String key, final byte[] value)
    public byte[] get(final byte[] key
    public String get(final String key)
    

    有了这些API的支持,就可以将Java对象序列化为二进制,当应用需要获取Java
    对象时, 使用get(final byte[] key)函数将字节数组取出,然后反序列化为
    Java对象即可。和很多NoSQL数据库的客户端不同,Jedis本身没有提供序列化
    的工具,也就是说开发者需要自己引入序列化的工具。序列化的工具有很多,例
    如XML、Json、谷歌的Protobuf、Facebook的Thrift等等,对于序列化工具的
    选择开发者可以根据自己需求决定。

  3. Jedis连接吃的使用方法

    上节介绍的是Jedis的直连方式,所谓直连是指Jedis每次都会新建TCP连接,使
    用后再断开连接,对于频繁访问Redis的场景显然不是高效的使用方式。因此生
    产环境中一般使用连接池的方式对Jedis连接进行管理,所有Jedis对象预先放
    在池子中(JedisPool),每次要连接Redis,只需要在池子中劫,用完了再归
    还给池子。

    客户端连接Redis使用的是TCP协议,直连的方式每次需要建立TCP连接,而连接
    池的方式可以预先初始化号Jedis连接,所以每次只需要从Jedis连接池借用即
    可,而借用和归还操作是在本地进行的,只有少量的并发同步开销,远远小于新
    建TCP连接的开销。另外智联的方式无法限制Jedis对象的个数,在极端情况下
    可能会造成连接泄露,而连接池的形式可以有效地保护和控制资源的使用。但是
    直连的方式也并不是一无是处,下表给出两种方式各自的优劣势。

    方式 优点 缺点
    直连 简单方便,适用于少量长期连接的场景 1.存在每次/关闭TCP连接开销 2.资源无法控制,极端情况会出现连接泄露 3.Jedis对象线程不安全
    连接池 1.无需每次连接都生成Jedis对象,降低开销 2.使用连接池的形式保护和控制资源的使用 相对于直连,使用相对麻烦,尤其在资源的管理上需要很多参数来保证,一旦规划不合理也会出现问题

    Jedis提供了JedisPool这个类作为对Jedis的连接池,同时使用了Apache的通
    用对象池工具common-pool作为资源的管理工具,下面是使用JedisPool操作
    Redis的代码实例:

    1) Jedis连接池(通常JedisPool是单例的):

    //common-pool连接池配置,这里使用默认配置,后面小节会介绍具体配置说明
    GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
    //初始化Jedis连接池
    JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);
    

    2)获取Jedis对象不再是直接生成一个Jedis对象进行直连,而是从连接池直接
    获取,代码如下:

    Jedis jedis = null;
    try{
        //1. 从连接池获取jedis对象
        jedis = jedisPool.getResource();
        //2. 执行操作
        jedis.get("hello");
    }catch (Exception e){
        logger.error(e.getMessage(), e);
    }finally{
        if(jedis != null){
            //如果适应JedisPool,close操作不是关闭连接,代表归还连接池
            jedis.close();
        }
    }
    

    这里可以看到在finally中依然是jedis.close()操作,为什么会把连接关闭
    呢,这不和连接池的原则违背了吗?但实际上Jedis的close()实现方式如下:

    public void close(){
        //使用Jedis连接池
        if(dataSource != null){
            if(client.isBroken){
                this.dataSource.returnBrokenResource(this);
            }else{
                this.dataSource.returnResource(this);
            }
        }else{
            client.close();
        }
    }
    

    参数说明:

    • dataSource != null代表使用的是连接池,所以jedis.close()代表兑换连
      接给连接池,而且Jedis会判断当前连接是否已经断开。

    • dataSource == null代表直连,jedis.close()代表关闭连接。

    前面GenericObjectPoolConfig使用的是默认配置,实际它提供有很多参数,
    例如池子中最大连接数、最大空闲连接数、最小空闲李娜结束、连接活性检测,
    等等,下表给出GenericObjectPoolConfig其他属性及其含义解释。

    参数名 含义 默认值
    maxActive 连接池中最大连接数 8
    maxIdle 连接池中最大空闲的连接数 8
    minIdle 连接池中最少空间的李娜结束 0
    maxWaitMillis 当连接池资源用尽后,调用者的最大等待时间(单位为毫秒),一般不建议使用默认值 -1,表示永远不超时,一直等。
    jmxEnabled 是否开启jmx监控,如果应用开启了jmx端口并且jmxEnabled设置为true,就可以通过jconsole或者jvisualvm看到关于连接池的相关统计,有助于了解连接池的使用情况,并且可以针对其做监控统计。 true
    minEvictableIdleTimeMillis 连接的最小空间时间,达到此值后空闲连接将被移除 1000L * 60L * 30毫秒 = 30分钟
    numTestsPerEvictionRun 做空间连接检测时,每次的采样数 3
    testOnBorrow 想连接池借用连接是否做连接有效性检测(ping),无效连接会被移除,每次借用多执行一次ping命令 false
    testOnReturn 向连接池归还连接时是否做连接有效性检测(ping),无效连接会被移除,每次归还多执行一次ping命令 false
    timeBetweenEvictionRunMillis 空闲连接的检测周期(单位为毫秒) -1:表示不做检测
    blockWhenExhausted 当连接池用尽后,调用者是否要等待,这个参数是和maxWaitMillis对应的,只有当此参数为true时,maxWaitMillis才会生效 true
  4. Redis中Pipeline的使用方法

    下面代码是借助Pipeline来模拟批量删除:

    public void mdel(List<String> keys){
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        //1)生成pipeline对象
        Pipeline pipeline = jedis.pipelined();
        //2)pipeline执行命令,注意此事命令并未真正执行
        for(String key : keys){
            pipeline.del(key);
        }
        //3)执行命令
        pipeline.sync();
    }
    

    除了pipeline。sync(),还可以使用pipeline.syncAndReturnAll()将
    pipeline的命令进行返回,例如下面代码将set和incr做了一次pipeline操
    作,并顺序打印了两个命令的结果:

    Jedis jedis = new Jedis("127.0.0.1", 6379);
    Pipeline pipeline = jedis.pipelined();
    pipeline.set("hello", "world");
    pipeline.incr("counter");
    List<Object> resultList = pipeline.syncAndReturnAll();
    for(Object object : resultList){
        System.out.println(object);
    }
    

    输出结果为:

    OK
    1
    
  5. Jedis的Lua脚本

    Jedis中执行Lua脚本和redis-cli十分类似,Jedis提供了三个重要的函数实现
    Lua脚本的执行:

    Object eval(String script, int key count, String ... params)
    Object evalSha(Stirng sha1, int keyCount, String ... params)
    String scriptLoad(String script)
    

    eval函数有三个参数,分别是:

    • script:Lua脚本内容。
    • keyCount:键的个数。
    • params:相关参数KEYS和ARGV。

    scriptLoad和evalSha函数要一起使用,首先使用scriptLoad将脚本加载到
    Redis中,evalSha函数用来执行脚本的SHA1校验和,它需要三个参数:

    • scriptSha:脚本的SHA1。
    • keyCount:键的个数。
    • params:相关参数KEYS和ARGV
  6. 重点注意以下几点:

    1) Jedis操作放在 try catch finally里更加合理。
    2) 区分直连和连接池两种实现方式的优缺点。
    3) jedis.close()方法的两种实现方式。
    4) Jedis依赖了common-pool
    5) 如果key和value涉及了字节数组,需要自己选择适合的序列化方法。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容