29-拓展 7:妙手仁心 —— 优雅地使用 Jedis
课程
1
开篇:授人以鱼不若授人以渔 —— Redis 可以用来做什么?
学习时长: 5分21秒
2
基础:万丈高楼平地起 —— Redis 基础数据结构
上次学到
学习时长: 16分14秒
3
应用 1:千帆竞发 —— 分布式锁
学习时长: 7分47秒
4
应用 2:缓兵之计 —— 延时队列
学习时长: 8分9秒
5
应用 3:节衣缩食 —— 位图
学习时长: 8分52秒
6
应用 4:四两拨千斤 —— HyperLogLog
学习时长: 14分17秒
7
应用 5:层峦叠嶂 —— 布隆过滤器
学习时长: 17分54秒
8
应用 6:断尾求生 —— 简单限流
学习时长: 4分37秒
9
应用 7:一毛不拔 —— 漏斗限流
学习时长: 7分22秒
10
应用 8:近水楼台 —— GeoHash
学习时长: 7分52秒
11
应用 9:大海捞针 —— Scan
学习时长: 8分42秒
12
原理 1:鞭辟入里 —— 线程 IO 模型
学习时长: 4分1秒
13
原理 2:交头接耳 —— 通信协议
学习时长: 3分34秒
14
原理 3:未雨绸缪 —— 持久化
学习时长: 5分27秒
15
原理 4:雷厉风行 —— 管道
学习时长: 3分51秒
16
原理 5:同舟共济 —— 事务
学习时长: 6分36秒
17
原理 6:小道消息 —— PubSub
学习时长: 7分7秒
18
原理 7:开源节流 —— 小对象压缩
学习时长: 7分14秒
19
原理 8:有备无患 —— 主从同步
学习时长: 4分9秒
20
集群 1:李代桃僵 —— Sentinel
学习时长: 3分52秒
21
集群 2:分而治之 —— Codis
学习时长: 7分28秒
22
集群 3:众志成城 —— Cluster
学习时长: 8分38秒
23
拓展 1:耳听八方 —— Stream
学习时长: 13分40秒
24
拓展 2:无所不知 —— Info 指令
学习时长: 4分4秒
25
拓展 3:拾遗补漏 —— 再谈分布式锁
学习时长: 2分18秒
26
拓展 4:朝生暮死 —— 过期策略
学习时长: 2分21秒
27
拓展 5:优胜劣汰 —— LRU
学习时长: 4分34秒
28
拓展 6:平波缓进 —— 懒惰删除
学习时长: 2分13秒
29
拓展 7:妙手仁心 —— 优雅地使用 Jedis
学习时长: 6分35秒
30
拓展 8:居安思危 —— 保护 Redis
学习时长: 2分19秒
31
拓展 9:隔墙有耳 —— Redis 安全通信
学习时长: 6分34秒
32
拓展 10:法力无边 —— Redis Lua 脚本执行原理
学习时长: 9分24秒
33
拓展 11:短小精悍 —— 命令行工具的妙用
学习时长: 9分21秒
34
源码 1:丝分缕析 —— 探索「字符串」内部
学习时长: 5分20秒
35
源码 2:循序渐进 —— 探索「字典」内部
学习时长: 7分24秒
36
源码 3:挨肩迭背 —— 探索「压缩列表」内部
学习时长: 10分42秒
37
源码 4:风驰电掣 —— 探索「快速列表」内部
学习时长: 3分49秒
38
源码 5:凌波微步 —— 探索「跳跃列表」内部
学习时长: 9分57秒
39
源码 6:破旧立新 —— 探索「紧凑列表」内部
学习时长: 2分42秒
40
源码 7:金枝玉叶 —— 探索「基数树」内部
学习时长: 5分36秒
41
源码 8:精益求精 —— LFU vs LRU
学习时长: 8分4秒
42
源码 9:如履薄冰 —— 懒惰删除的巨大牺牲
学习时长: 9分53秒
43
源码 10:跋山涉水 —— 深入字典遍历
学习时长: 9分24秒
44
源码 11:见缝插针 —— 探索 HyperLogLog 内部
学习时长: 13分3秒
45
尾声:百尺竿头 —— 继续深造指南
学习时长: 2分32秒
juejin_logo copyCreated with Sketch.

拓展 7:妙手仁心 —— 优雅地使用 Jedis

本节面向 Java 用户,主题是如何优雅地使用 Jedis 编写应用程序,既可以让代码看起来赏心悦目,又可以避免使用者犯错。

Jedis 是 Java 用户最常用的 Redis 开源客户端。它非常小巧,实现原理也很简单,最重要的是很稳定,而且使用的方法参数名称和官方的文档非常 match,如果有什么方法不会用,直接参考官方的指令文档阅读一下就会了,省去了非必要的重复学习成本。不像有些客户端把方法名称都换了,虽然表面上给读者带来了便捷,但是需要挨个重新学习这些 API,提高了学习成本。

Java 程序一般都是多线程的应用程序,意味着我们很少直接使用 Jedis,而是要用到 Jedis 的连接池 —— JedisPool。同时因为 Jedis 对象不是线程安全的,当我们要使用 Jedis 对象时,需要从连接池中拿出一个 Jedis 对象独占,使用完毕后再将这个对象还给连接池。

用代码表示如下:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class JedisTest {

  public static void main(String[] args) {
    JedisPool pool = new JedisPool();
    Jedis jedis = pool.getResource(); // 拿出 Jedis 链接对象
    doSomething(jedis);
    jedis.close(); // 归还链接
  }

  private static void doSomething(Jedis jedis) {
    // code it here
  }

}

上面的代码有个问题,如果 doSomething 方法抛出了异常的话,从连接池中拿出来的 Jedis 对象将无法归还给连接池。如果这样的异常发生了好几次,连接池中的所有链接都被持久占用了,新的请求过来时就会阻塞等待空闲的链接,这样的阻塞一般会直接导致应用程序卡死。

为了避免这种情况的发生,程序员需要在使用 JedisPool 里面的 Jedis 链接时,应该使用 try-with-resource 语句来保护 Jedis 对象。

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class JedisTest {

  public static void main(String[] args) {
    JedisPool pool = new JedisPool();
    try (Jedis jedis = pool.getResource()) { // 用完自动 close
      doSomething(jedis);
    }
  }

  private static void doSomething(Jedis jedis) {
    // code it here
  }

}

这样 Jedis 对象肯定会归还给连接池 (死循环除外),避免应用程序卡死的惨剧发生。

但是当一个团队够大的时候,并不是所有的程序员都会非常有经验,他们可能因为各种原因忘记了使用 try-with-resource 语句,惨剧就会突然冒出来让运维人员措手不及。我们需要在代码上加上一层硬约束,通过这层约束,当程序员想要访问 Jedis 对象时,不会再出现使用了 Jedis 对象而不归还。

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

interface CallWithJedis {
  public void call(Jedis jedis);
}

class RedisPool {

  private JedisPool pool;

  public RedisPool() {
    this.pool = new JedisPool();
  }

  public void execute(CallWithJedis caller) {
    try (Jedis jedis = pool.getResource()) {
      caller.call(jedis);
    }
  }

}

public class JedisTest {

  public static void main(String[] args) {
    RedisPool redis = new RedisPool();
    redis.execute(new CallWithJedis() {

      @Override
      public void call(Jedis jedis) {
        // do something with jedis
      }

    });
  }

}

我们通过一个特殊的自定义的 RedisPool 对象将 JedisPool 对象隐藏起来,避免程序员直接使用它的 getResource 方法而忘记了归还。程序员使用 RedisPool 对象时需要提供一个回调类来才能使用 Jedis 对象。

但是每次访问 Redis 都需要写一个回调类,真是特别繁琐,代码也显得非常臃肿。幸好 Java8 带来了 Lambda 表达式,我们可以使用 Lambda 表达式简化上面的代码。

public class JedisTest {

  public static void main(String[] args) {
    Redis redis = new Redis();
    redis.execute(jedis -> {
      // do something with jedis
    });
  }

}

这样看起来就简洁优雅多了。但是还有个问题,Java 不允许在闭包里修改闭包外面的变量。比如下面的代码,我们想从 Redis 里面拿到某个 zset 对象的长度,编译器会直接报错。

public class JedisTest {

  public static void main(String[] args) {
    Redis redis = new Redis();
    long count = 0;
    redis.execute(jedis -> {
      count = jedis.zcard("codehole");  // 此处应该报错
    });
    System.out.println(count);
  }

}

编译器暴露出来的错误时:Local variable count defined in an enclosing scope must be final or effectively final,告诉我们 count 变量必须设置成 final 类型才可以让闭包来访问。

如果这时我们将 count 设置成 final 类型,结果编辑器又报错了:The final local variable count cannot be assigned. It must be blank and not using a compound assignment,告诉我们 final 类型的变量在闭包里面不能被修改。

那该怎么办呢?

这里需要定义一个 Holder 类型,将需要修改的变量包装起来。

class Holder<T> {
  private T value;

  public Holder() {
  }

  public Holder(T value) {
    this.value = value;
  }

  public void value(T value) {
    this.value = value;
  }

  public T value() {
    return value;
  }
}

public class JedisTest {

  public static void main(String[] args) {
    Redis redis = new Redis();
    Holder<Long> countHolder = new Holder<>();
    redis.execute(jedis -> {
      long count = jedis.zcard("codehole");
      countHolder.value(count);
    });
    System.out.println(countHolder.value());
  }

}

有了上面定义的 Holder 包装类,就可以绕过闭包对变量修改的限制。只不过代码上要多一层略显繁琐的变量包装过程。这些都是对程序员的硬约束,他们必须这么做才可以得到自己想要的数据。

重试

我们知道 Jedis 默认没有提供重试机制,意味着如果网络出现了抖动,就会大范围报错,或者一个后台应用因为链接过于空闲被服务端强制关闭了链接,当重新发起新请求时就第一个指令会出错。而 Redis 的 Python 客户端 redis-py 提供了这种重试机制,redis-py 在遇到链接错误时会尝试进行重连,然后再重发指令。

那如果我们希望在 Jedis 上面增加重试机制,该如何做呢?有了上面的 RedisPool 对象,重试就非常容易进行了。

class Redis {

  private JedisPool pool;

  public Redis() {
    this.pool = new JedisPool();
  }

  public void execute(CallWithJedis caller) {
    Jedis jedis = pool.getResource();
    try {
      caller.call(jedis);
    } catch (JedisConnectionException e) {
      caller.call(jedis);  // 重试一次
    } finally {
      jedis.close();
    }
  }

}

上面的代码我们只重试了一次,如有需要也可以重试多次,但是也不能无限重试,就好比人逝不可复生,要节哀顺变。

作业

囿于精力,以上代码并没有做到非常细致,比如 Redis 的链接参数都没有提及,连接池的大小以及超时参数等也没有配置,这些细节工作就留给读者们作为本节的作业,自己动手完成一个完善的封装吧。

留言
Ctrl + Enter
全部评论(19)
BinK_1783的头像
删除
生命的意义,在于人与人的相互照亮
点赞
回复
黄钏的头像
删除
Lambda表达式那块是不是写错了,new了一个Redis,应该new RedisPool把
1
回复
贞菜的头像
删除
Python 控,鉴定完毕!
点赞
1
删除
而 Redis 的 Python 客户端 redis-py 提供了这种重试机制,redis-py 在遇到链接错误时会尝试进行重连,然后再重发指令。
点赞
回复
七分冷酷丶的头像
删除
全栈工程师 @ 北京互融云
这是多线程环境下,最近发生一个问题,用了jdk1.7的try新特性,还是会报一个错,就是类型转换错误,java.lang.ClassCastException: [B cannot be cast to java.lang.Long,后来查资料,得知原因是redis操作出现异常,jedis实例中缓存数据不会清空,而是直接放回连接池,下一次从池子中取出同一个jedis对象,发送的命令用的还是上一个线程的数据,请问,有解吗,如何解决
1
回复
Subfire的头像
删除
no
Redis redis = new Redis(); // 是 new JedisPool() 吧
点赞
回复
木易本尊60286的头像
删除
印象中有看过这篇文章,在微信公众号没找到,回来翻手册又看到了,得经常复习
点赞
回复
jacksonchu的头像
删除
spring boot 的 redisTemplate 就是差不多这个逻辑
1
回复
Thompson酱的头像
删除
JedisPool不应该是单例的么??每个用户都自己new一个??
点赞
1
删除
实际开发中,类似RedisPool这样的类,通常会交给spring管理,spring的实例默认单例的
点赞
回复
pengmingfa521的头像
删除
学习了~
点赞
回复
钱同志的头像
删除
开始以为是个最佳实践来着,阅后发现老钱这是启发思考
3
回复
YHL重名了的头像
删除
应该用动态代理实现,这样比较简单方便
点赞
回复
linuxea的头像
删除
著名开发工程师
很好
点赞
回复
LiaoXY的头像
删除
后台开发工程师 @ 菜鸟网络
闭包不是js里面的概念吗....
点赞
1
删除
你只能说你在js中页见到过闭包..
点赞
回复
橙子本尊51347的头像
删除
哈哈哈哈,最后一句话好逗
点赞
2
删除
兄台,敢问是那句话呢?
点赞
回复
删除
人逝不可复生,要节哀顺变
点赞
回复