侧边栏壁纸
博主头像
why

一个主要敲代码,经常怼文章,偶尔拍视频的成都人。

  • 累计撰写 197 篇文章
  • 累计创建 11 个标签
  • 累计收到 101 条评论

构建一个高效且可伸缩的缓存,最后一集。

why
why
2022-02-24 / 0 评论 / 3 点赞 / 326 阅读 / 5,936 字
温馨提示:
关注公众号why技术,第一时间接收最新文章。

你好呀,我是歪歪。

怎么回事啊,小老弟,我又要来填坑来啦。

周二的时候发布了《构建高效且可伸缩的结果缓存》这篇文章。

文章开始的时候我就说了,这个方案的演进过程是按照《Java并发编程实战》的第 5.6 节的内容写的。

有的同学去看了书上的内容,找过来对我说:歪哥,不对啊,你这个和书上不一样啊。

肯定不一样啊,老铁。

思路是一样的,但是内容要还是一样了我还写个啥啊,粘过来不就完事了。

我那篇文章的写作过程是先掌握了大师的方案演进过程,然后按照自己的理解写了一版我认为更加通俗易懂的文章出来。

当然,我不是说大师写的不好,我只是觉得书上的代码有点太过抽象了,所以我才带入了一个查询考研分数的例子去把这个抽象的代码给具象了。

所以,我给出的最终代码是这样的:

public class ScoreQueryService {

    public static final Map<String, Future<Integer>> SCORE_CACHE = new ConcurrentHashMap<>();

    public Integer query(String userName) throws Exception {
        while (true) {
            Future<Integer> future = SCORE_CACHE.get(userName);
            if (future == null) {
                Callable<Integer> callable = () -> loadFormDB(userName);
                FutureTask futureTask = new FutureTask<>(callable);
                future = SCORE_CACHE.putIfAbsent(userName, futureTask);
                //如果为空说明之前这个 key 在 map 里面不存在
                if (future == null) {
                    future = futureTask;
                    futureTask.run();
                }
            }
            try {
                return future.get();
            } catch (CancellationException e) {
                System.out.println("查询userName=" + userName + "的任务被移除");
                SCORE_CACHE.remove(userName, future);
            } catch (Exception e) {
                throw e;
            }
        }
    }

    private Integer loadFormDB(String userName) throws InterruptedException {
        System.out.println("开始查询userName=" + userName + "的分数");
        //模拟耗时
        TimeUnit.SECONDS.sleep(5);
        return ThreadLocalRandom.current().nextInt(380, 420);
    }
}

而书上的最终代码是这样的:

代码确实不一样,但是你不觉得神似吗?

书上给出的是一个通用缓存的解决方案,所以你可以看到很多泛型的东西在里面。

好的,如果你确实是想看书上的这个代码是怎么来的,那么我给你解释一下。

垃圾代码

相比于大师在书中给出的代码,我的代码确实很垃圾。

垃圾在什么地方?

完全没有任何扩展性,也就是传说中的“死代码”。

你想啊,构建一个高效且可伸缩的结果缓存这个方案,说到底应该是一个思路。只要你是有响应的缓存场景,那么这个思路就完全可以拿过去往上套。

而我给出的代码,就已经和“查询考研分数”这个场景绑定死了。如果后续要来一个完全不相干的场景,比如“查询核酸检测结果”,那么我是不是还得按照这个思路再写一份类似的代码?

扩展性垃圾的一比。

那么怎么去提升扩展性呢?

想,来转动你的小脑壳,想一想哪个设计模式比较适合这个场景呢?

是不是一下就想到了装饰模式?

好吧,说“是”的同学,别装了,这玩意,凭我这几句描述,是很难想到的。

我也是看了大师的代码才能想到装饰模式的。

不装逼了,直接看代码。

装饰模式

装饰模式,首先是搞一个抽象接口嘛,比如参照书上的命名,我写个这样似儿的:

public interface Computable<A, V> {
    V compute(A arg) throws Exception;
}

Computable,这个词儿应该认识吧,四级必考词汇,得学起来:

比如我还是查分这个场景,我的实现类应该是这样的:

public class ScoreQuery implements Computable<String, Integer> {

    @Override
    public Integer compute(String userName) throws Exception {
        System.out.println("开始查询userName=" + userName + "的分数");
        //模拟耗时
        TimeUnit.SECONDS.sleep(5);
        return ThreadLocalRandom.current().nextInt(380, 420);
    }
}

有些猴急的同学看了上面的代码,马上就发出疑问了:但是你的 ScoreQuery 这玩意也没缓存功能啊?

别着急啊,我这不是,马上就写嘛。

缓存功能,我们可以给它扩展一下,包装包装不就有了吗?

也就是搞个装饰器。

关于装饰器,书中用到的名字是 Memoizer,为啥是这个名字,也给了一个解释:

这个 Memoizer 装饰类应该长什么样呢?

首先 Memoizer 得实现 Computable 接口,然后重写其 compute 方法:

public class Memoizer<A, V> implements Computable<A, V> {
    
    @Override
    public V compute(A arg) throws Exception {
        return null;
    }
}

然后就得想:怎么在 compute 里面搞事情呢?

它只是一个装饰而已,它装饰的是真正的业务类。而这个业务类也需要实现 Computable,比如我们前面出现的 ScoreQuery 类:

假设 ScoreQuery 类就是被装饰的业务类,那么 Memoizer 也必然要调用 ScoreQuery 的 compute 方法。

怎么调用?

它肯定得自己持有一个 ScoreQuery 对象才行嘛。

但是直接持有一个 ScoreQuery 对象又显得很不妥当了,因为扩展性就没了。

但是换个思路,让它持有一个 Computable 对象,扩展性就出来了。

所以,它应该是这样的:

public class Memoizer<A, V> implements Computable<A, V> {

    private final Computable<A, V> c;

    public Memoizer(Computable<A, V> c) {
        this.c = c;
    }

    @Override
    public V compute(A arg) throws Exception {
        return c.compute(arg);
    }
}

这样就“装饰”的基本架子就搭好了。

啥,你说你咋没看出来?

说明你小子对装饰模式掌握欠缺,得去加深一下。

我告诉你这个装饰器怎么用:

上面代码截图中,被注释的代码就是不用装饰的代码。

所谓的“用装饰器”,不过就是这样的一行代码而已:

Memoizer memoizer = new Memoizer(new ScoreQuery());

用 Memoizer 这个通用类,来装饰了 ScoreQuery 这个有具体含义的类。

但是把程序跑起来之后你会发现 ScoreQuery 的 compute 方法被执行了三次:

说明什么情况?

就说明缓存没生效嘛。

当然没生效了,因为 Memoizer 装饰类里面根本就没有缓存相关的代码呀。

所以,Memoizer 类里面还得搞一个缓存也就是 map,并且把它给用起来,

public class Memoizer<A, V> implements Computable<A, V> {
    
    private final Map<A, V> cache = new HashMap<A, V>();
    
    private final Computable<A, V> c;
    
    public Memoizer1(Computable<A, V> c) {
        this.c = c;
    }
    
    public V compute(A arg) throws Exception {
        V result = cache.get(arg);
        if (result == null) {
            result = c.compute(arg);
            cache.put(arg, result);
        }
        return result;
    }
}

代码就变成了这样。

好家伙,到这里不就是书上的第一版的代码吗?

唯一的区别就是少了一个 synchronized 而已。

而到这里,后续的就是方案演进的过程了,这个过程已经在《构建高效且可伸缩的结果缓存》这篇文章里面写的很详细了。

无非就是从 synchronized 换成 ConcurrentHashMap,然后再引入 FutureTask,最后使用 putIfAbsent 方法,这一套组合拳打下来,最终的代码就是这样的:

class Memoizer<A, V> implements Computable<A, V> {
    
    private final ConcurrentMap<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
    
    private final Computable<A, V> c;

    public Memoizer(Computable<A, V> c) {
        this.c = c;
    }

    @Override
    public V compute(final A arg) throws Exception {
        while (true) {
            Future<V> f = cache.get(arg);
            if (f == null) {
                Callable<V> callable = new Callable<V>() {
                    @Override
                    public V call() throws Exception {
                        return c.compute(arg);
                    }
                };
                FutureTask<V> ft = new FutureTask<V>(callable);
                f = cache.putIfAbsent(arg, ft);
                if (f == null) {
                    f = ft;
                    ft.run();
                }
            }
            try {
                return f.get();
            } catch (CancellationException e) {
                cache.remove(arg, f);
            } catch (ExecutionException e) {
                throw e;
            }
        }
    }
}

现在具有高效且可伸缩的结果缓存能力的装饰器算是就位了,用起来也很简单:

从上面的程序输出结果来说,“装饰”成功。

Memoizer 给 ScoreQuery 赋能了缓存功能,而且属于润物细无声的那种,完全屏蔽了实现细节。

而且扩展性很强嘛。

比如我要改成缓存核酸检测的结果,只需要把实现了 Computable 接口,并且在 compute 方法中编写了获取核算检测结果逻辑的类,假设这个类叫做 NucleicAcidTest,那么用法就是这样的:

Memoizer memoizer = new Memoizer(new NucleicAcidTest());

是不是扩展性一下就体现出来了。

这才更加符合文章的标题“构建高效且可伸缩的结果缓存”。

这是一种对缓存的抽象思想,而我落地的时候直接带入了一个场景进去,所以写出了最开始那种没有任何扩展性的“死”代码。

和书里面给出来的装饰器模式的代码一比,我承认确实是垃圾代码。

但是从写文章的角度来说,好吧,不狡辩了,也是垃圾。

人家书写的确实牛逼,全是干货,没啥废话。

《Java并发编程实战》这本书,我都推荐过很多次了,我再推荐一次。

这本书经常和《Java并发编程的艺术》放在一起对比,很多读者问我到底看哪本好?

那么这两本书到底谁好一点呢?

这个问题很难去回答,《Java并发编程的艺术》是中国人写的,可能读起来顺畅一些。《Java并发编程实战》是译本,有的时候外国人的思维和语言用中文译出来后表达的意思就没有那么精准了。

所以如果你一定要我给出一个建议,那么我的建议是:两本都买,先读《艺术》再读《实战》。

当然,如果你的英文非常牛皮的话,直接读《Java编发编程实战》的英文版。

逼格拉满,还不怕翻译问题。

对了,说到翻译,我就想起了单词,说到单词,前面在不经意间你又学到了一个高频短语:

核酸检测,Nucleic Acid Test。

下次约人见面的时候你就可以说:give me show show your Nucleic Acid Test,I must check check.

说个题外话

最后,说到装饰者模式我想到了多年前入门 Java 的时候,看黑马毕向东老师的课程。

他讲到 io 这一部分的时候,就讲到了装饰者模式。

当时属于连 Java 的门在哪都没摸到的角色,懵懵懂懂听到设计模式的时候当然是听不懂的。

虽然听不懂,但是不妨碍我照模照样的敲一个出来。

所以装饰者模式确实是我学到过的、亲手敲过的第一个设计模式,我对它还是比较亲切的。

写到这里的时候我还去 B 站上又看了一遍毕老师讲这一节的视频,是第 19 天。

时隔多年,再次听到毕老师那熟悉的声音,看到这个充满年代感的记事本界面。

啊,这该死的熟悉感,这就是我梦开始的地方。

我想说的,当年第一次学到“装饰者模式”的时候,我根本不知道它是干啥的,即使我把代码背下来了,也没有任何卵用。

但是现在,我只需要看一眼书上的代码,我就知道它用的是装饰着模式。

所以学习的路上,不要在意一城一池的得失,要锱铢必较,也要有的放矢。要接受自己的平庸,也要相信自己的坚持。

要知道一年级的时候它是一道附加题,但是到了六年级的时候它就是一道送分题了。

共勉之吧。

最后说一句

没想到啊,没想到啊。按照我的尿性,一般来说一个话题有个上下集就很不错了。

而一个话题我硬是写了上中下三篇文章,除了这一篇外,还有这两篇:

《构建高效且可伸缩的结果缓存》

《当Synchronized遇到这玩意儿,有个大坑,要注意!》

回想上次一个话题写三篇的时候,还是上次啊。

就是上次赔了 16w 那次:

《几行烂代码,我赔了16万。》

《仔细思考之后,发现只需要赔6w。》

《我就辣鸡怎么了?》

哎...

0

评论区